diff --git a/docs/casa/administration/quick-start.md b/docs/casa/administration/quick-start.md index 7bee1058172..d462898f27a 100644 --- a/docs/casa/administration/quick-start.md +++ b/docs/casa/administration/quick-start.md @@ -32,7 +32,6 @@ First, the custom authentication scripts corresponding to the authentication met Scripts can be more easily looked up by display name. The below table shows the display names of the scripts previously listed: -|-|-| |Script|Display name| |-|-| |OTP (SMS)|twilio_sms| diff --git a/docs/casa/developer/add-authn-methods.md b/docs/casa/developer/add-authn-methods.md new file mode 100644 index 00000000000..90b545fdf37 --- /dev/null +++ b/docs/casa/developer/add-authn-methods.md @@ -0,0 +1,103 @@ +# Onboarding custom authentication methods + +Out-of-the-box Casa supports some useful authentication methods for a secure, pleasant authentication experience. Adding more authentication mechanisms is possible and requires certain development effort. In this guide we summarize the steps required to do so and give you some useful pointers to start your coding journey. + +Supporting a new authentication mechanisms consists of two tasks: coding a custom interception script and creating a plugin that contributes an authentication method. The former has to do with the authentication flow the user experiences (to access Casa or other apps), and the latter with the credential enrollment process. + +## Interception script + +!!! Note + Acquaintance with [person authentication](../../admin/developer/scripts/person-authentication.md) scripts is required + +### About Casa authentication flow + +Casa's authentication is backed by a main custom script that determines which authentication methods are currently supported, dynamically imports relevant custom scripts, and orchestrates the general flow while delegating specific implementation details to some of those scripts. + +The main script supports backtracking: if a user is asked to present a specific credential and that credential isn't currently available, he can choose an alternative credential by visiting a different page corresponding to the alternative authentication method. Users can backtrack any number of times. + +### Script requisites + +To code the script corresponding to the authentication method to add, use the `.py` script found [here](https://github.com/JanssenProject/jans/tree/main/jans-casa/plugins/samples/sample-cred) as a canvas. Ensure the following conditions are met so that it properly integrates in the main Casa flow: + +- For step 1, `prepareForStep` must only return `True` +- For step 1, `getExtraParametersForStep` must only return `None` +- For step 1, the `authenticate` routine must check if there is already an authenticated user, and if so bypass validating the username and password. This is because a user may have previously attempted authentication with a different method +- `hasEnrollments` routine has a signature like: + `def hasEnrollments(self, configurationAttributes, user):` + where the `configurationAttributes` parameter is a `java.util.Map` and `user` is an instance belonging to `io.jans.as.common.model.common.User` +- `hasEnrollments` must return `True` or `False`, describing whether `user` has one or more credentials enrolled for the type you are interested in +- Keep in mind that `getPageForStep` won't be called when `step=1` in your script. Casa takes charge of this specific step/method combination +- Finally, ensure that custom pages returned by `getPageForStep` for step 2 (or higher) contain the fragment: + + ``` + + ``` + + This will display a set of links for the user to navigate to alternate 2FA pages. The list will be shown when clicking on a link which should be provided this way: + + ``` + #{msgs['casa.alternative']} + ``` + + Here `ELEMENT_ID` is the identifier for the HTML node that wraps all visual elements of your page (excluding `casa.xhtml`). It is required to preserve `alter_link` as `id` for the `a` tag. + +Adding the script to the server can be done via [TUI](../../admin/config-guide/config-tools/jans-tui/README.md) for instance. Ensure the display name assigned to the script is short and meaningful. Check [here](../administration/quick-start.md#enable-scripts) for examples. This value will be used as "ACR" in the plugin that will have to be developed for the credential enrollment process. + +### Key questions + +As you code the script, you will come up with some design decisions, for instance: + +- How to model and store credentials associated to the authentication method? +- What kind of parameters are relevant for the authentication method? +- What's the algorithm for authenticating users once they have supplied a valid username/password combination? + +Depending on the answers, you may like to start instead with plugin development first. This is not always the case though, however, getting your hands on the plugin might help unclutter the path. + +## Enrollment plugin + +Coding a Casa plugin is mainly a Java development task. You can use the "Sample credential" [plugin](https://github.com/JanssenProject/jans/tree/main/jans-casa/plugins/samples/sample-cred) as a template to start the work. Ensure you have a development environment with: + +- Java 11 or higher +- Maven 3.8 +- A SSH client tool +- Access to a Jans Server installation that includes Jans Casa. Prefer a VM installation over the CN edition for development purposes + +### Plugin deployment + +Start with deploying the plugin to get acquainted with the process: + +1. Download the `sample-cred` project folder to the local development machine and `cd` to it. You can download the jans repository [here](https://github.com/JanssenProject/jans/archive/refs/heads/main.zip) +1. Run `mvn -o -Dmaven.test.skip package` +1. This will generate a `target` folder with a couple of jar files in it + +Access Casa admin console and in the plugins page, upload the file suffixed with `jar-with-dependencies.jar`. After one minute approximately, in a browser hit `https:///jans-casa/pl/sample-cred-plugin/user/cred_details.zul`. You will get access to a dummy page. + +You can remove the plugin and add it as many times as you like - no restarts are needed. You can do so either via the admin console or by dropping/removing the file directly in the filesystem (the path is `/opt/jans/jetty/jans-casa/plugins`) + +### Study the sample project + +Now it's time for you to go through the project folder checking one file at a time. Most of files contain comments that explain the purpose of things. + +Once you are done, analyze the file `./src/main/resources/assets/user/cred_details.zul`. It contains the markup of the page you visited earlier. + +### Enable the authentication method + +Once the [interception script](#interception-script) is added to the server (a draft is OK), visit the admin console and enable the [authentication method](../administration/quick-start.md#enable-methods-in-casa) by assigning the plugin just loaded. From here onwards, the left-hand side menu of Casa will have a new item under the "2FA credentials" heading. Additionally a new panel in the user's dashboard will appear and show some detail about the authentication method. + +### Additional tweaks + +If you alter the `.zul` file and then package and redeploy the plugin, you will most probably not see any change taking effect in the UI page. This is because the ZK framework caches the `.zul` pages by default for a very long period. To change this behavior do the following: + +1. Connect to your VM and `cd` to `/opt/gluu/jetty/jans-casa/webapps` +1. Extract ZK descriptor: + ``` + # jar -xf jans-casa.war WEB-INF/zk.xml + ``` +1. Locate XML tag `file-check-period` and remove it including its surrounding parent `desktop-config` +1. Save the file and patch the application war: + ``` + # jar -uf jans-casa.war WEB-INF/zk.xml + ``` +1. Restart casa (e.g. `systemctl casa restart`) + +From now on, any template change will take effect after 5 seconds. diff --git a/docs/casa/index.md b/docs/casa/index.md index 45365348681..7615764d606 100644 --- a/docs/casa/index.md +++ b/docs/casa/index.md @@ -54,6 +54,8 @@ Casa is a plugin-oriented, Java web application. Existing functionality can be e - [Custom branding](./plugins/custom-branding.md) - [2FA settings](./plugins/2fa-settings.md) +If you are interested in onboarding additional authentication methods to Casa, read this [guide](./developer/add-authn-methods.md). + ## Janssen Server integration Janssen Server relies on "interception scripts" to implement user authentication. Casa itself has an interception script which defines authentication logic and routes authentications to specific 2FA mechanisms which also have their own scripts. diff --git a/jans-casa/plugins/samples/helloworld/src/main/java/io/jans/casa/plugins/helloworld/HelloWorldMenu.java b/jans-casa/plugins/samples/helloworld/src/main/java/io/jans/casa/plugins/helloworld/HelloWorldMenu.java index 0644218f182..c3fa0f2f509 100644 --- a/jans-casa/plugins/samples/helloworld/src/main/java/io/jans/casa/plugins/helloworld/HelloWorldMenu.java +++ b/jans-casa/plugins/samples/helloworld/src/main/java/io/jans/casa/plugins/helloworld/HelloWorldMenu.java @@ -5,21 +5,26 @@ import org.pf4j.Extension; /** - * An extension class implementing the {@link NavigationMenu} extension point. + * Represents a menu item to be added to Casa navigation menu * @author jgomer */ @Extension public class HelloWorldMenu implements NavigationMenu { public String getContentsUrl() { + //Location of the template that holds the markup of the menu item to be added. + //See the resource/assets directory return "menu.zul"; } public MenuType menuType() { + //Whether this menu item is to be added to the general user menu or the admin-only menu return MenuType.USER; } public float getPriority() { + //A numeric value employed to sort the menu items. Items with higher priority appear + //first in the menu return 0.5f; } diff --git a/jans-casa/plugins/samples/helloworld/src/main/resources/assets/menu.zul b/jans-casa/plugins/samples/helloworld/src/main/resources/assets/menu.zul index eff04f6790f..40421d6bbb0 100644 --- a/jans-casa/plugins/samples/helloworld/src/main/resources/assets/menu.zul +++ b/jans-casa/plugins/samples/helloworld/src/main/resources/assets/menu.zul @@ -1,9 +1,18 @@
  • + + + + + ${labels.hello.title} +
  • diff --git a/jans-casa/plugins/samples/sample-cred-plugin/README.md b/jans-casa/plugins/samples/sample-cred-plugin/README.md deleted file mode 100644 index 16dfc030d11..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Sample plugin for enrolling credentials - -Steps to run the plugin - -1. Enable "basic" custom script in oxTrust -2. In the custom script for basic - add a new property 2fa_requisite = true -3. Log in to casa, in casa admin console, go to "Enabled authentication methods" from the menu. Select "basic" as a 2fa method for authentication. -4. Add the plugin jar file from the admin console -5. Notice the newly created menu that reads "sample device" in the menu bar diff --git a/jans-casa/plugins/samples/sample-cred-plugin/pom.xml b/jans-casa/plugins/samples/sample-cred-plugin/pom.xml deleted file mode 100644 index e70bceb7aa9..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/pom.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - 4.0.0 - - io.jans.casa.plugins - ${plugin.id} - 1.0.22-SNAPSHOT - jar - - - 11 - 11 - sample-plugin - - - - - jans - Janssen project repository - https://maven.jans.io/maven - - - - - - - org.apache.maven.plugins - maven-assembly-plugin - 3.1.0 - - - jar-with-dependencies - - - - ${plugin.id} - ${project.version} - Janssen project - io.jans.casa.plugins.sample.SamplePlugin - - This is a sample plugin used for enrolling a 2fa credential - - Available under Apache 2 License - io.jans.casa.plugins - - - - - - make-assembly - package - - single - - - - - - - - - - io.jans - casa-shared - ${project.version} - provided - - - - - \ No newline at end of file diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/credentials/extensions/SampleExtension.java b/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/credentials/extensions/SampleExtension.java deleted file mode 100644 index 1034ee7f057..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/credentials/extensions/SampleExtension.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.jans.casa.plugins.credentials.extensions; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import io.jans.casa.credential.BasicCredential; -import io.jans.casa.extension.AuthnMethod; -import io.jans.casa.misc.Utils; -import io.jans.casa.plugins.sample.SampleService; -import io.jans.casa.service.ISessionContext; -import org.pf4j.Extension; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author madhumita - * - */ -@Extension -public class SampleExtension implements AuthnMethod { - - private SampleService sampleService; - - private Logger logger = LoggerFactory.getLogger(getClass()); - private ISessionContext sessionContext; - - public SampleExtension() { - sessionContext = Utils.managedBean(ISessionContext.class); - sampleService = SampleService.getInstance(); - } - - public String getUINameKey() { - - return "sample_label"; - } - - public String getAcr() { - return sampleService.getInstance().ACR; - } - - public String getPanelTitleKey() { - return "sample_title"; - } - - public String getPanelTextKey() { - return "sample_text"; - } - - public String getPanelButtonKey() { - - return "sample_manage"; - } - - public String getPanelBottomTextKey() { - return "sample_download"; - } - - public String getPageUrl() { - return "user/cred_details.zul"; - - } - - public List getEnrolledCreds(String id) { - //pass user name or anything that uniquely identifies a user - String userName = sessionContext.getLoggedUser().getUserName(); - - try { - return sampleService.getInstance().getDevices(userName).stream() - .map(dev -> new BasicCredential(dev.getNickName(), 0)).collect(Collectors.toList()); - } catch (Exception e) { - logger.error(e.getMessage(), e); - return Collections.emptyList(); - } - - } - - public int getTotalUserCreds(String id) { - //pass user name or anything that uniquely identifies a user - String userName = sessionContext.getLoggedUser().getUserName(); - return sampleService.getInstance().getDeviceTotal(userName); - } - - public void reloadConfiguration() { - SampleService.getInstance().reloadConfiguration(); - - } - - public boolean mayBe2faActivationRequisite() { - return Boolean.parseBoolean(Optional - .ofNullable(SampleService.getInstance().getScriptPropertyValue("2fa_requisite")).orElse("false")); - } - -} diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/credentials/extensions/SampleFragment.java b/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/credentials/extensions/SampleFragment.java deleted file mode 100644 index 70f071e9705..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/credentials/extensions/SampleFragment.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.jans.casa.plugins.credentials.extensions; - -import io.jans.casa.extension.PreferredMethodFragment; -import org.pf4j.Extension; - -/** - * An extension class implementing the {@link PreferredMethodFragment} extension point. It allows to insert extra markup - * when users have enabled 2fa in their accounts. - * @author madhumita - */ -@Extension -public class SampleFragment implements PreferredMethodFragment -{ - - public String getUrl() { - return "index.zul"; - } - -} diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/credentials/extensions/SampleMenu.java b/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/credentials/extensions/SampleMenu.java deleted file mode 100644 index 08764973777..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/credentials/extensions/SampleMenu.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.jans.casa.plugins.credentials.extensions; - -import io.jans.casa.extension.navigation.MenuType; -import io.jans.casa.extension.navigation.NavigationMenu; -import org.pf4j.Extension; - - -/** - * @author madhumita - * - */ -@Extension -public class SampleMenu implements NavigationMenu { - - public String getContentsUrl() { - return "menu.zul"; - } - - public MenuType menuType() { - return MenuType.USER; - } - - public float getPriority() { - return 0.8f; - } - -} diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SampleInitiator.java b/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SampleInitiator.java deleted file mode 100644 index c8aa4349024..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SampleInitiator.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.jans.casa.plugins.sample; - -import java.util.Map; - -import org.zkoss.zk.ui.Page; -import org.zkoss.zk.ui.util.Initiator; - -public class SampleInitiator implements Initiator{ - - @Override - public void doInit(Page page, Map args) throws Exception { - } - -} diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SamplePlugin.java b/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SamplePlugin.java deleted file mode 100644 index c72a174ea24..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SamplePlugin.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.jans.casa.plugins.sample; - -import io.jans.casa.core.ITrackable; -import org.pf4j.Plugin; -import org.pf4j.PluginWrapper; - -/** - * A plugin for handling second factor authentication settings for administrators and users. - * @author jgomer - */ -public class SamplePlugin extends Plugin implements ITrackable { - - public SamplePlugin(PluginWrapper wrapper) { - super(wrapper); - } - - -} diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SampleService.java b/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SampleService.java deleted file mode 100644 index dfaa3bc4b66..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SampleService.java +++ /dev/null @@ -1,92 +0,0 @@ -package io.jans.casa.plugins.sample; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import io.jans.casa.credential.BasicCredential; -import io.jans.casa.misc.Utils; -import io.jans.casa.service.IPersistenceService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.zkoss.bind.annotation.NotifyChange; - -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Class that holds the logic to list and enroll sample creds - * - * @author madhumita - * - */ - -public class SampleService { - - private static SampleService SINGLE_INSTANCE = null; - public static Map properties; - private Logger logger = LoggerFactory.getLogger(getClass()); - public static String ACR = "basic"; - - private IPersistenceService persistenceService; - - private SampleService() { - persistenceService = Utils.managedBean(IPersistenceService.class); - reloadConfiguration(); - - } - - public static SampleService getInstance() { - if (SINGLE_INSTANCE == null) { - synchronized (SampleService.class) { - SINGLE_INSTANCE = new SampleService(); - } - } - return SINGLE_INSTANCE; - } - - public void reloadConfiguration() { - ObjectMapper mapper = new ObjectMapper(); - properties = persistenceService.getCustScriptConfigProperties(ACR); - if (properties == null) { - logger.warn( - "Config. properties for custom script '{}' could not be read. Features related to {} will not be accessible", - ACR, ACR.toUpperCase()); - } else { - try { - logger.info("Sample settings found were: {}", mapper.writeValueAsString(properties)); - } catch (Exception e) { - logger.error(e.getMessage(), e); - } - } - } - - public String getScriptPropertyValue(String value) { - return properties.get(value); - } - - public List getDevices(String uniqueIdOfTheUser) { - // Write the code to connect to the 3rd party API and fetch credentials against - // the user - List list = new ArrayList(); - list.add(new BasicCredential("test device 1", System.currentTimeMillis())); - list.add(new BasicCredential("test device 2", System.currentTimeMillis())); - return list; - } - - public int getDeviceTotal(String uniqueIdOfTheUser) { - // Write the code to connect to the 3rd party API and fetch total number of - // credentials against the user - return 0; - } - - public boolean deleteSampleDevice(String userName, String deviceId) { - // write the logic for deleting the device - return true; - } - - public boolean updateSampleDevice(String userName, String oldName, String newName) { - // write the logic for updating the device - return true; - } - -} diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SampleVM.java b/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SampleVM.java deleted file mode 100644 index 79f7b01f7dc..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/java/io/jans/casa/plugins/sample/SampleVM.java +++ /dev/null @@ -1,227 +0,0 @@ -package io.jans.casa.plugins.sample; - -import java.util.ArrayList; -import java.util.List; - -import io.jans.casa.credential.BasicCredential; -import io.jans.casa.misc.Utils; -import io.jans.casa.service.ISessionContext; -import io.jans.casa.ui.UIUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.zkoss.bind.BindUtils; -import org.zkoss.bind.annotation.Init; -import org.zkoss.bind.annotation.NotifyChange; -import org.zkoss.json.JavaScriptValue; -import org.zkoss.util.Pair; -import org.zkoss.util.resource.Labels; -import org.zkoss.zk.au.out.AuInvoke; -import org.zkoss.zk.ui.event.Event; -import org.zkoss.zk.ui.event.Events; -import org.zkoss.zk.ui.select.annotation.WireVariable; -import org.zkoss.zk.ui.util.Clients; -import org.zkoss.zul.Messagebox; - -/** - * @author madhumita - * - */ - -public class SampleVM { - - private Logger logger = LoggerFactory.getLogger(getClass()); - private static final int QR_SCAN_TIMEOUT = 60; - @WireVariable - private ISessionContext sessionContext; - - private List devices; - private BasicCredential newDevice; - private String activationCode; // this is the code that gets projected inside the QR code - private boolean uiQRShown; - - private String editingId; - - public boolean isUiQRShown() { - return uiQRShown; - } - - public void setUiQRShown(boolean uiQRShown) { - this.uiQRShown = uiQRShown; - } - - public String getActivationCode() { - return activationCode; - } - - public void setActivationCode(String activationCode) { - this.activationCode = activationCode; - } - - public BasicCredential getNewDevice() { - return newDevice; - } - - public void setNewDevice(BasicCredential newDevice) { - this.newDevice = newDevice; - } - - public String getEditingId() { - return editingId; - } - - public void setEditingId(String editingId) { - this.editingId = editingId; - } - - public List getDevices() { - return devices; - } - - /** - * Initialization method for this ViewModel. - */ - @Init - public void init() { - sessionContext = Utils.managedBean(ISessionContext.class); - // fetch devices from SampleService - devices = SampleService.getInstance().getDevices(sessionContext.getLoggedUser().getUserName()); - - } - - public void delete(BasicCredential device) { - Pair delMessages = getDeleteMessages(device.getNickName(), null); - - Messagebox.show(delMessages.getY(), delMessages.getX(), Messagebox.YES | Messagebox.NO, - true ? Messagebox.EXCLAMATION : Messagebox.QUESTION, event -> { - if (Messagebox.ON_YES.equals(event.getName())) { - try { - - boolean result = SampleService.getInstance().deleteSampleDevice( - sessionContext.getLoggedUser().getUserName(), device.getNickName()); - if (result == false) { - UIUtils.showMessageUI(false); - } else { - devices.remove(device); - // trigger refresh (this method is asynchronous...) - BindUtils.postNotifyChange(SampleVM.this, "devices"); - UIUtils.showMessageUI(true); - } - - } catch (Exception e) { - UIUtils.showMessageUI(false); - logger.error(e.getMessage(), e); - } - } - }); - } - - Pair getDeleteMessages(String nick, String extraMessage) { - - StringBuilder text = new StringBuilder(); - if (extraMessage != null) { - text.append(extraMessage).append("\n\n"); - } - text.append(Labels.getLabel("sample_del_confirm", - new String[] { nick == null ? Labels.getLabel("general.no_named") : nick })); - if (extraMessage != null) { - text.append("\n"); - } - - return new Pair<>(Labels.getLabel("sample_del_title"), text.toString()); - } - - @NotifyChange({ "devices", "editingId", "newDevice" }) - public void update() { - - String newName = newDevice.getNickName(); - if (Utils.isNotEmpty(newName)) { - // Find the index of the current device in the device list - int i = Utils.firstTrue(devices, dev -> String.valueOf(dev.getNickName()).equalsIgnoreCase(editingId)); - BasicCredential dev = devices.get(i); - - // TODO:set the new name - // dev.setNickName(newName); - cancelUpdate(null); // This doesn't undo anything we already did (just controls UI aspects) - - try { - boolean result = SampleService.getInstance() - .updateSampleDevice(sessionContext.getLoggedUser().getUserName(), dev.getNickName(), newName); - if (result == false) { - UIUtils.showMessageUI(false); - } else { - UIUtils.showMessageUI(true); - devices = SampleService.getInstance().getDevices(sessionContext.getLoggedUser().getUserName()); - // devices.remove(i); - // devices.add( dev); - // trigger for refresh - BindUtils.postNotifyChange(SampleVM.this, "devices"); - UIUtils.showMessageUI(true); - } - } catch (Exception e) { - UIUtils.showMessageUI(false); - logger.error(e.getMessage(), e); - } - } - - } - - @NotifyChange({ "editingId", "newDevice" }) - public void cancelUpdate(Event event) { - editingId = null; - if (event != null && event.getName().equals(Events.ON_CLOSE)) { - event.stopPropagation(); - } - } - - public void showQR() { - - try { - - //TODO: compute the logic for the contents of the QR code - activationCode = "1234"; - uiQRShown = true; - BindUtils.postNotifyChange(this, "uiQRShown"); - - // Passing screen width as max allowed size for QR code allows showing QRs even - // in small mobile devices - // TODO:remove hardcoding (screen width) - JavaScriptValue jvalue = new JavaScriptValue(getFormattedQROptions(30)); - // Calls the startQR javascript function supplying suitable params - Clients.response(new AuInvoke("startQR", activationCode, "", jvalue)); - Clients.scrollBy(0, 10); - - } catch (Exception e) { - UIUtils.showMessageUI(false); - logger.error(e.getMessage(), e); - } - - } - - public String getFormattedQROptions(int maxWidth) { - - List list = new ArrayList<>(); - int size = 20;// getQrSize(); - int ival = maxWidth > 0 ? Math.min(size, maxWidth - 30) : size; - - if (ival > 0) { - list.add("size:" + ival); - } - - double dval = 0.05; // getQrMSize(); - if (dval > 0) { - list.add("mSize: " + dval); - } - - return list.toString().replaceFirst("\\[", "{").replaceFirst("\\]", "}"); - - } - - @NotifyChange({"newDevice", "editingId"}) - public void prepareForUpdate(BasicCredential dev) { - //This will make the modal window to become visible - editingId = String.valueOf(dev.getNickName()); - newDevice = new BasicCredential(dev.getNickName(),0); - //newDevice.setNickName(dev.getNickName()); - - } -} diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/admin/menu.zul b/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/admin/menu.zul deleted file mode 100644 index 596cb528285..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/admin/menu.zul +++ /dev/null @@ -1,10 +0,0 @@ - - -
  • - - ${labels.sample_title}-this is the "admin" view - -
  • - -
    diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/images/sample.png b/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/images/sample.png deleted file mode 100644 index a1748b395df..00000000000 Binary files a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/images/sample.png and /dev/null differ diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/index.zul b/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/index.zul deleted file mode 100644 index d3b00e317c7..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/index.zul +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/menu.zul b/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/menu.zul deleted file mode 100644 index 92c6575879c..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/menu.zul +++ /dev/null @@ -1,10 +0,0 @@ - - -
  • - - - ${labels.sample.title} - this is "user" view - -
  • - -
    diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/user/cred_details.zul b/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/user/cred_details.zul deleted file mode 100644 index d00b064e0b7..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/assets/user/cred_details.zul +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - ${zkService.appName} - ${labels.sample_title} - - - - - -
    - - -
    -
    - -
    -

    - ${labels.sample_title} -

    -

    ${labels.sample_text}

    -
    - - - - -
    -
    - -

    - -
    - -

    -
    -
    - - - - - - -
    - -
    -
    -
    - - -
    -

    ${labels.sample_add.device.title}

    - -

    ${labels.sample_install_ready}

    -
    - - - -
    -

    ${labels.sample_download}

    - - - - - -
    - ${labels.general.new_nick} -
    - -
    -
    -
    - - -
    -
    -
    -
    - - - - - - - - - - diff --git a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/labels/zk-label.properties b/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/labels/zk-label.properties deleted file mode 100644 index ce8d50e6a63..00000000000 --- a/jans-casa/plugins/samples/sample-cred-plugin/src/main/resources/labels/zk-label.properties +++ /dev/null @@ -1,33 +0,0 @@ -# Charset for this file must be UTF-8 -sample_label=sample devices -sample_title=sample devices -sample_text=sample devices include a mobile authentication app, that will authenticate seamlessly or send you push notifications to approve or deny at each login -sample_manage=Manage sample devices -sample_download=Download sample Authenticator from AppStore or Google Play -sample_link_appstore= -sample_link_googleplay= - -sample_scan=Scan the QR code and tap on approve -sample_close_automatic=This window closes automatically upon successful enrollment -sample_install_ready=Make sure sample mobile app is installed in your smart phone. Open the app and press the button below when you are ready. -sample_edit=Change nickname of your sample device -sample_del=Remove this device from your list credentials -sample_already_enrolled=This device is already enrolled! - -#labels for activating a new device -sample_add.device.title=To activate a new device now - - -#Utility labels -enter_nick=Enter a nickname for this device -you_added=You have already enrolled: -sample_del_title=Remove this device from your list of credentials -sample_del_confirm=You are about to remove {0}, proceed? - -del_conflict_revert=If you remove this device your preferred mechanism will be reset to password because {0} -del_conflict_underflow=the number of enrolled credentials after removal will be less than {0} (the minimum required to use strong authentication). -del_conflict_preferred=this is the only device that matches your preferred method of authentication. -del_conflict_requisite=it is required you have at least one credential belonging to: {0}. - -enroll.success=Your device has been successfully added -enroll.error=Your device could not be enrolled - diff --git a/jans-casa/plugins/samples/sample-cred/README.md b/jans-casa/plugins/samples/sample-cred/README.md new file mode 100644 index 00000000000..c41a5084961 --- /dev/null +++ b/jans-casa/plugins/samples/sample-cred/README.md @@ -0,0 +1,14 @@ +# Sample cred plugin + +This folder contains a Casa plugin that can be used as a starting point to add a new authentication mechanism to Casa. The plugin is a maven project (so Java knowledge is required) and covers introductory aspects for enrollment logic. + +Note authentication logic is performed at the authorization server - not Casa. For this, creating a person authentication [jython script](https://docs.jans.io/head/admin/developer/scripts/person-authentication/) and building custom pages are necessary. Use the `script.py` file found in this directory to start coding. + +In both scenarios, enrollment and authentication, you will resort to checking how already supported authentication methods work. These tasks will demand skills related to: + +- Python and Java +- HTML development +- Java Server Faces +- [ZK](https://www.zkoss.org/) framework 9 + +Happy coding! diff --git a/jans-casa/plugins/samples/sample-cred/pom.xml b/jans-casa/plugins/samples/sample-cred/pom.xml new file mode 100644 index 00000000000..f5f8a2fc731 --- /dev/null +++ b/jans-casa/plugins/samples/sample-cred/pom.xml @@ -0,0 +1,77 @@ + + + + 4.0.0 + + io.jans.casa.plugins + ${plugin.id} + 1.0.22-SNAPSHOT + jar + + + 11 + 11 + sample-cred-plugin + + + + + jans + Janssen project repository + https://maven.jans.io/maven + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.1.0 + + + jar-with-dependencies + + + + ${plugin.id} + ${project.version} + Developer name + io.jans.casa.plugins.SampleCredentialPlugin + + This plugin is a template for developers needing to add an authentication method + to Jans-Casa + + Apache 2 + io.jans.casa.plugins + + + + + + make-assembly + package + + single + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.2.0 + + + + + + + io.jans + casa-shared + ${project.version} + provided + + + + \ No newline at end of file diff --git a/jans-casa/plugins/samples/sample-cred/script.py b/jans-casa/plugins/samples/sample-cred/script.py new file mode 100644 index 00000000000..6cc0300b12e --- /dev/null +++ b/jans-casa/plugins/samples/sample-cred/script.py @@ -0,0 +1,96 @@ +from io.jans.jsf2.message import FacesMessages +from io.jans.as.server.security import Identity +from io.jans.as.server.service import AuthenticationService +from io.jans.model.custom.script.type.auth import PersonAuthenticationType +from io.jans.service.cdi.util import CdiUtil +from io.jans.util import StringHelper + +from jakarta.faces.application import FacesMessage + +import sys + +class PersonAuthentication(PersonAuthenticationType): + def __init__(self, currentTimeMillis): + self.currentTimeMillis = currentTimeMillis + + def init(self, customScript, configurationAttributes): + print "Sample cred. Initialized" + return True + + def destroy(self, configurationAttributes): + print "Sample cred. Destroyed successfully" + return True + + def getApiVersion(self): + return 11 + + def getAuthenticationMethodClaims(self, configurationAttributes): + return None + + def isValidAuthenticationMethod(self, usageType, configurationAttributes): + return True + + def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): + return None + + def authenticate(self, configurationAttributes, requestParameters, step): + + print "Sample cred. Authenticate for Step %s" % str(step) + identity = CdiUtil.bean(Identity) + authenticationService = CdiUtil.bean(AuthenticationService) + user = authenticationService.getAuthenticatedUser() + + if step == 1: + + if user == None: + credentials = identity.getCredentials() + user_name = credentials.getUsername() + user_password = credentials.getPassword() + + if StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password): + authenticationService.authenticate(user_name, user_password) + user = authenticationService.getAuthenticatedUser() + + if user == None: + return False + + #Additional authn logic for step 1 must go here + else: + if user == None: + return False + + #Additional authn logic for step 2 must go here + + #facesMessages = CdiUtil.bean(FacesMessages) + #facesMessages.setKeepMessages() + #facesMessages.clear() + #facesMessages.add(FacesMessage.SEVERITY_ERROR, "Wrong code entered") + + return False + + + def prepareForStep(self, configurationAttributes, requestParameters, step): + print "Sample cred. Prepare for Step %s" % str(step) + return True + + def getExtraParametersForStep(self, configurationAttributes, step): + return None + + def getCountAuthenticationSteps(self, configurationAttributes): + print "Sample cred. getCountAuthenticationSteps called" + return 2 + + def getPageForStep(self, configurationAttributes, step): + print "Sample cred. getPageForStep called %s" % step + if step == 2: + # you are supposed to build this page and place it in an accessible location + return "/casa/sample_cred/page.xhtml" + return "" + + def logout(self, configurationAttributes, requestParameters): + return True + + def hasEnrollments(self, configurationAttributes, user): + # whether user has one or more credentials enrolled for this type of credential + return True + diff --git a/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/SampleCredentialPlugin.java b/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/SampleCredentialPlugin.java new file mode 100644 index 00000000000..eba7bb3a72d --- /dev/null +++ b/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/SampleCredentialPlugin.java @@ -0,0 +1,31 @@ +package io.jans.casa.plugins; + +import org.pf4j.Plugin; +import org.pf4j.PluginWrapper; + +public class SampleCredentialPlugin extends Plugin { + + public SampleCredentialPlugin(PluginWrapper wrapper) { + super(wrapper); + //Add code here for initialization-related tasks. Good candidates are: + // - Initialization of singleton variables + // - Obtain application-scoped bean references + // - One-time computations + } + + @Override + public void start() { + //Plugins have a lifecycle (see https://pf4j.org/doc/plugins.html) + //In Casa we follow a simpler approach: when a plugin archive is added, + //the plugin is instantiated and then started. If these operations succeeded, + //the plugin can then be removed - in this case it will be stopped and then deleted + + //Use this method for additional initialization tasks not covered in the constructor + } + + @Override + public void delete() { + //Use this method for clean-up related duties + } + +} diff --git a/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/sample/extension/SampleCredentialAuthnMethod.java b/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/sample/extension/SampleCredentialAuthnMethod.java new file mode 100644 index 00000000000..ab1f356feac --- /dev/null +++ b/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/sample/extension/SampleCredentialAuthnMethod.java @@ -0,0 +1,77 @@ +package io.jans.casa.plugins.sample.extension; + +import io.jans.casa.credential.BasicCredential; +import io.jans.casa.extension.AuthnMethod; +import io.jans.casa.plugins.sample.service.SampleCredentialService; + +import java.util.*; + +import org.pf4j.Extension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Extension +public class SampleCredentialAuthnMethod implements AuthnMethod { + + private SampleCredentialService credService; + private Logger logger = LoggerFactory.getLogger(getClass()); + + public SampleCredentialAuthnMethod() { + credService = SampleCredentialService.getInstance(); + } + + public String getUINameKey() { + //see zk-label.properties + return "sample.method_label"; + } + + public String getPanelTitleKey() { + //see zk-label.properties + return "sample.method_title"; + } + + public String getPanelTextKey() { + //see zk-label.properties + return "sample.method_text"; + } + + public String getPanelButtonKey() { + //see zk-label.properties + return "sample.method_button_label"; + } + + public String getPageUrl() { + //Path (relative to the plugin's base url) to the page where management of these credentials will take place + return "user/cred_details.zul"; + } + + public String getAcr() { + //ACR associated to this type of credential. A custom script has to exist in the server + //and must use this acr as display name + return credService.ACR; + } + + public void reloadConfiguration() { + //See javadocs of method reloadConfiguration in + //https://github.com/JanssenProject/jans/blob/main/jans-casa/shared/src/main/java/io/jans/casa/extension/AuthnMethod.java + + //credService.reloadConfiguration(); + } + + public List getEnrolledCreds(String id) { + //See javadocs of method getEnrolledCreds in + //https://github.com/JanssenProject/jans/blob/main/jans-casa/shared/src/main/java/io/jans/casa/extension/AuthnMethod.java + + //return credService.getEnrolledCreds(id); + return Collections.emptyList(); + } + + public int getTotalUserCreds(String id) { + //See javadocs of method getTotalUserCreds in + //https://github.com/JanssenProject/jans/blob/main/jans-casa/shared/src/main/java/io/jans/casa/extension/AuthnMethod.java + + //return credService.getTotalUserCreds(id); + return 0; + } + +} diff --git a/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/sample/service/SampleCredentialService.java b/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/sample/service/SampleCredentialService.java new file mode 100644 index 00000000000..6f3302a01ab --- /dev/null +++ b/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/sample/service/SampleCredentialService.java @@ -0,0 +1,62 @@ +package io.jans.casa.plugins.sample.service; + +import io.jans.casa.credential.BasicCredential; +import io.jans.casa.misc.Utils; +import io.jans.casa.service.IPersistenceService; + +import java.util.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class handles the CRUD (management) of credentials + */ +public class SampleCredentialService { + + public static String ACR = "sample_cred_acr"; + + private IPersistenceService persistenceService; + private Logger logger = LoggerFactory.getLogger(getClass()); + + private Map properties; + private static SampleCredentialService SINGLE_INSTANCE = null; + + private SampleCredentialService() { + persistenceService = Utils.managedBean(IPersistenceService.class); + reloadConfiguration(); + } + + public static SampleCredentialService getInstance() { + + if (SINGLE_INSTANCE == null) { + SINGLE_INSTANCE = new SampleCredentialService(); + } + return SINGLE_INSTANCE; + + } + + public void reloadConfiguration() { + //Retrieve configuration properties of the script, if any + properties = persistenceService.getCustScriptConfigProperties(ACR); + //Put other initialization stuff here + } + + public List getEnrolledCreds(String id) { + //Code the logic required to build a list of the credentials already enrolled + //by the user whose unique identifier is id + + return Collections.emptyList(); + } + + public int getTotalUserCreds(String id) { + //Code the logic required to compute the number of the credentials already enrolled + //by the user whose unique identifier is id. Calling size over the returned value of + //method getEnrolledCreds is an option + return 0; + } + + //Likely, other methods for credential manipulation will go here. + //These would be called from class SampleCredentialVM which handles UI interaction + +} diff --git a/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/sample/vm/SampleCredentialVM.java b/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/sample/vm/SampleCredentialVM.java new file mode 100644 index 00000000000..af4a13be36e --- /dev/null +++ b/jans-casa/plugins/samples/sample-cred/src/main/java/io/jans/casa/plugins/sample/vm/SampleCredentialVM.java @@ -0,0 +1,36 @@ +package io.jans.casa.plugins.sample.vm; + +import io.jans.casa.core.pojo.User; +import io.jans.casa.plugins.sample.service.SampleCredentialService; +import io.jans.casa.service.ISessionContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.zkoss.bind.annotation.Init; +import org.zkoss.zk.ui.select.annotation.WireVariable; + +/** + * ZK ViewModel associated to this plugin's cred-details.zul page. See the viewModel attribute in + * panel component of cred-details.zul. In Casa, the MVVM (model-view-viewModel) design pattern is used + */ +public class SampleCredentialVM { + + private Logger logger = LoggerFactory.getLogger(getClass()); + + @WireVariable + private ISessionContext sessionContext; + + private User user; + private SampleCredentialService credService; + + @Init + public void init() { + logger.info("ViewModel inited"); + user = sessionContext.getLoggedUser(); + credService = SampleCredentialService.getInstance(); + } + + //Add more methods as required for the zul page to do what it is supposed to do + //This requires knowledge of ZK framework. Existing pages and classes in Casa can be used as a guide + +} diff --git a/jans-casa/plugins/samples/sample-cred/src/main/resources/assets/user/cred_details.zul b/jans-casa/plugins/samples/sample-cred/src/main/resources/assets/user/cred_details.zul new file mode 100644 index 00000000000..1895ae78cd5 --- /dev/null +++ b/jans-casa/plugins/samples/sample-cred/src/main/resources/assets/user/cred_details.zul @@ -0,0 +1,42 @@ + + + + + + + ${zkService.appName} - ${labels.sample.method_label} + + + + + + + +
    +
    + +
    +

    ${labels.sample.method_title}

    +

    ${labels.sample.method_text}

    +

    Ensure you have added the custom script for this authentication method in the server and that it was enabled under "Enabled authentication methods" here in Casa admin console

    +
    + + + +
    TODO
    + +
    +
    + +
    + + + + + + +
    diff --git a/jans-casa/plugins/samples/sample-cred/src/main/resources/labels/zk-label.properties b/jans-casa/plugins/samples/sample-cred/src/main/resources/labels/zk-label.properties new file mode 100644 index 00000000000..10749f1e0eb --- /dev/null +++ b/jans-casa/plugins/samples/sample-cred/src/main/resources/labels/zk-label.properties @@ -0,0 +1,18 @@ +# Charset for this file must be UTF-8 + +# Lines starting with a hash sign are ignored and can be used for documentation purposes + +# Text to use for the left-hand side menu item under the big "2FA" credentials heading. Normally short and in plural, eg. Smart Cards +sample.menu_label=Credential type name (plural) + +# "Official" name of the credential type. Often in singular, eg. Smart Card +sample.method_label=Name of your credential type + +# Title used in the panel associated to this method in Casa users' dashboard, eg. Smart Cards +sample.method_title=Panel title + +# Summary shown in the panel associated to this method in Casa users' dashboard, eg. Credit card-sized devices with an embedded chip used to control access to resources +sample.method_text=A description of what/how this kind of credential works + +# Text used in the button that will take users to the specific page for managing this kind of credentials +sample.method_button_label=Manage credential diff --git a/mkdocs.yml b/mkdocs.yml index 38cf27eb1fc..bf62ec1c855 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8815,6 +8815,8 @@ nav: - 'Consent Management': 'casa/plugins/consent-management.md' - 'Custom Branding': 'casa/plugins/custom-branding.md' - 'FAQ': 'casa/administration/faq.md' + - Developer Guide: + - 'Adding authentication methods': 'casa/developer/add-authn-methods.md' - User Guide: 'casa/user-guide.md' plugins: - tags