Java


Table of Contents:

Prerequisites
Java SDK Installation
Run the Java Demo App
Creating your own Java App


Prerequisites


Creating an app involves two basic steps:

  1. Register/create an app in the portal and obtain credentials (this is detailed in the 'Register / Create a New App' menu section)
  2. Configure your app using an SDK, making use of these credentials

It is recommended that you first create a demo/test app with a redirect uri of http://127.0.0.1:5000/login in the portal, to enable you to run the SDK demo app and test the SDK functionality.

In the portal, when you create a new app, you will be issued with the client ID and client secret credentials that you need to specify when building your app with the SDK. These are found in the app settings screen:

app settings page

Note that you can control the Login Methods available (QR Code requires customer usage of the mobile app. While Browser Login enables logging in within the desktop browser, without the mobile app)

When creating your app in the portal, you also must specify a redirect_uri endpoint which comes from the url where you are hosting your app:

add app

The SDK will automatically create the redirect uri by appending '/login' to your url. You, of course, are in charge of what the base url of your app is. So it could be http://testapp.com/login, http://12.34.567.89:8080/login or http://127.0.0.1:5000/login as above. Note, however, that the MFA server will not accept 'localhost' as a base url.


Java SDK Installation


1. Dependencies:

  • Java version 1.8+
  • Apache Maven
  • Gradle build tool

2. Download / Clone the SDK:

First of all, download the zip of the repo from https://github.com/miracl/maas-sdk-java.git or clone it:

git clone https://github.com/miracl/maas-sdk-java

Now open the miracl.json file found in sample-spark/src/main/resources/and configure it with your client id, client secret and redirect_uri values, as set up in the Create a New App menu section:

For security, the client ID and secret for a genuine app should not be stored in clear text in a config file. This has only been done here for simple demo purposes. For a production scenario, the client ID and secret should be programmatically accessed via an encrypted API

{
  "client_id": "CLIENT_ID",
  "secret": "SECRET",
  "redirect_uri": "http://127.0.0.1:5000/login",
  "serverPort": 5000,
  "use_proxy": false,
  "proxy_host": "PROXY_HOST",
  "proxy_port": "PROXY_PORT"
}

Note that the port on which your app is served can be changed, plus a proxy can be configured by changing use_proxy to true, and entering the host and port details.

Then open the sample-spark/src/main/resources/templates/index.pebble file and, towards the end of the file, make sure the mpad script has the correct url in order to communicate with the authentication server (note that it begins with 'mcl.cdn.mpin.io'):

<script src="https://mcl.cdn.mpin.io/mpad/mpad.js" data-authurl="{{ authURL }}" data-element="btmpin"></script>

Now make sure you are in the maas-sdk-java root folder and, before you run the demo, build the SDK to make it a reusable dependency for other projects:

./gradlew maas-sdk:publishToMavenLocal

This installs the SDK as an artifact to the local Maven repository. The maas-sdk directory can also be used as a subproject in a Gradle project.


Run the Java demo app


Running the pre-configured demo app will help you make sure that you have all your settings correct and so will be able to proceed with confidence toward developing your own application.

Note that the demo app is configured to run on 127.0.0.1:5000. This is controlled by the line port(5000); in /java/com/miracl/maas_samples/SparkSample.java. This means that the demo app will, by default, work with an app that has been set up in the portal with a redirect uri of http://127.0.0.1:5000/login. If you wish to change this, just edit the port(5000); line.

  1. ./gradlew sample-spark:run

    Your app should now be running on http://127.0.0.1:5000/ (or whichever port you have set!)

    login_button

  2. From here you will be prompted to:

    1. Register
    2. Key in your identity
    3. Confirm your identity (by email activation)
    4. Create a 4-digit PIN
    5. Login

    Once logged in you will be greeted by the refresh/logout page:

    image2


Creating your own Java app


What follows is a detailed breakdown of how the demo app was created, which will assist you in creating your own bespoke app and integrating the login functionality with your services.

Note that, as well as Apache Maven and Gradle, the demo app makes use of the Java pebble templating engine and the Spark web application micro framework. The information presented here should give you enough information to begin creating an app with any templating engine or framework.

The following illustrates the file structure of the project which is stored in a sample-spark folder at the top level of the maas-sdk-java root folder:

java tree

The .class files within the build folder will have been created automatically when you ran the ./gradlew sample-spark:run command.

The details of the build can be found in the build.gradle file.

Within the sample-spark/src/main folder, the key files we are concerned with are:

Java code file:

  • /java/com/miracl/maas_samples/SparkSample.java

Config and html files:

  • /resources/miracl.json
  • /resources/templates/index.pebble

1. Create the main web page and the login button

Note that the use of col-md divs here assumes use of the bootstrap framework, as does the use of the {% %} tags to receive flash messages in the standard bootstrap categories of 'success', 'info' and 'danger'.

In your /resources/templates/index.pebble file, first listen for messages coming from your main script:


<div class="row">
    <div class="col-md-12">
        {% if messages is not empty %}
            {% for message in messages %}
                <div class="alert alert-{{ message.category }}">
                    {{ message.message }}
                </div>
            {% endfor %}
        {% endif %}
    </div>
</div>

Then run an if retry to check for the retry message coming from your main script. The retry message means that an attempted login has failed (as will be seen later) and your script will have provided an authorization url. In this case a btmpin div is created to contain the login button (note that the wording for this is automatically populated by mpad.js hence there is no text entered in the btmpin div):

{% if retry %}
    <div class="col-md-12">
        <div id="btmpin"></div>
    </div>
{% end %}

The next if is_authorized check is to listen for the is_authorized message, in which case display refresh or logout buttons which initiate a redirect to the "/refresh" and "/logout" endpoints:


{% else %}
    <div class="row">
        {% if authorized %}
            <div class="col-md-4">
                <b>E-mail:</b> {{ email }}<br/>
                <b>User ID:</b> {{ userId }}<br/>
            </div>
            <div class="col-md-4"></div>
            <div class="col-md-4">
                <a href="/refresh" class="btn btn-primary action">Refresh user data</a>
                <a href="/logout" class="btn btn-primary action">Log out</a>
            </div>
        {% else %}
            <div class="col-md-12">
                <div class="col-md-8" id="example-email">
                    <div class="form-group">
                        <label for="example-email-input" class="col-xs-2 col-form-label">Log In As:</label>
                        <div class="col-xs-10">
                            <input class="form-control" type="email" id="example-email-input" placeholder="email address">
                        </div>
                    </div>
                </div>
                <div id="btmpin"></div>
            </div>
        {% endif %}
    </div>

Note that the refresh functionality which is configured in your main script can be used to manage user timeouts.

The last else section of the above snippet creates an email input box which makes use of the prerollid javascript which is found at the top of the file:

<script type="application/javascript">
    $(document).ready(function() {
        $("input[type=email]").keyup(function() {
            var email = $("input[type=email]").val();
            $("#btmpin").attr("data-prerollid", email);
        });
    });
</script>

This enables capturing the user's email address to bake it into the QR code. Ultimately, it will then be pre-populated in the user's phone app, saving them the trouble of entering their email address twice and enhancing the user experience.

A btmpin div is also created to contain the login button (note that the wording for this is automatically populated by mpad.js hence there is no text entered in the btmpin div).

Finally, just before the closing </body> tag, include the script for the login button:

<script src="https://mcl.cdn.mpin.io/mpad/mpad.js" data-authurl="{{ auth_url }}" data-element="btmpin"></script>  

This makes use of the mpad.js library to send the authentication information to the server:

The parameters passed in this script are:

  • data-element: the login button ID (corresponds with <div id="btmpin">)
  • data-authurl: the authorization URL (this passes the client_id and redirect_uri to the authentication server). Each SDK has a 'Get Authorization Request URL' method for obtaining this.

2. Create a Credentials file

For security, the client ID and secret for a genuine app should not be stored in clear text in a config file. This has only been done here for simple demo purposes. For a production scenario, the client ID and secret should be programmatically accessed via an encrypted API

In your resources folder, create a file named miracl.json to store the credentials for your app:

{
  "client_id": "CLIENT_ID",
  "secret": "SECRET",
  "redirect_uri": "REDIRECT_URL",
  "serverPort": 5000,
  "use_proxy": false,
  "proxy_host": "PROXY_HOST",
  "proxy_port": "PROXY_PORT"
}

Substitute the correct values for your app, as discussed in 'Prerequisites' above.

3. Create the main Java file

Now, in the /java/com/miracl/maas_samples/SparkSample.java file:

  1. Key Imports

    package com.miracl.maas_samples;
    
    import com.eclipsesource.json.Json;
    import com.eclipsesource.json.JsonObject;
    import com.miracl.maas_sdk.JwtValidator;
    import com.miracl.maas_sdk.MiraclClient;
    import com.miracl.maas_sdk.MiraclException;
    import com.mitchellbosecke.pebble.PebbleEngine;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.logging.Level;
    import java.util.logging.Logger;
    
    import spark.ModelAndView;
    import spark.Request;
    import spark.Response;
    import spark.Session;
    import spark.TemplateEngine;
    import spark.template.pebble.PebbleTemplateEngine;
    
    import static spark.Spark.get;
    import static spark.Spark.port;
  2. Setup message handling and template rendering for communication with index.pebble

    This is set up before the main code section.

    First of all set up the ModelAndView for rendering to index.pebble and receiving the messages ArrayList and a HashMap for storage and retrieval of user data and messages:

    public static ModelAndView prepareModelAndView(Session session, Map<String, Object> data) {
        // Create model with data passed to function
        Map<String, Object> params = new HashMap<>(data);
    
        // Add messages from session to model
        params.put(MESSAGES_ATTRIBUTE, session.attribute(MESSAGES_ATTRIBUTE));
        // Clean messages from session (consider messages viewed)
        session.removeAttribute(MESSAGES_ATTRIBUTE);
    
        // Add missing flags (defaulting to false)
        if (!params.containsKey(RETRY_ATTRIBUTE)) {
            params.put(RETRY_ATTRIBUTE, false);
        }
        if (!params.containsKey(AUTHORIZED_ATTRIBUTE)) {
            params.put(AUTHORIZED_ATTRIBUTE, false);
        }
    
        return new ModelAndView(params, "templates/index.pebble");
    }  

    Then an ArrayList is set up for saving of the messages which come from the results of the authorization checks later in the code:

    public static void flashMessage(Session session, String category, String message) {
        // Retrieving messages from session
        ArrayList<Map<String, String>> messages = session.attribute(MESSAGES_ATTRIBUTE);
    
        // Creating new list if session did not contain messages
        if (messages == null) {
            messages = new ArrayList<>();
        }
    
        // Creating message object
        final HashMap<String, String> messageMap = new HashMap<>();
        messageMap.put("category", category);
        messageMap.put("message", message);
    
        // Adding message to messages list
        messages.add(messageMap);
    
        // Setting sessions messages to new messages list
        session.attribute(MESSAGES_ATTRIBUTE, messages);
    }
  3. Setup Handlers to manage Authorization Flow

    At the main request handler ("/") endpoint, a check is made to see if the user is already authorised by using the isAuthorized method:

    // Main request handler - show login button or user data
    private static ModelAndView handleMainRequest(MiraclClient client, Request req) {
        // Construct session wrapper for Miracl
        final MiraclSparkSessionWrapper preserver = new MiraclSparkSessionWrapper(req.session());
    
        // Model data for template
        Map<String, Object> data = new HashMap<>();
    
        // Check if session have information about user
        final boolean authorized = client.isAuthorized(preserver);
        data.put(AUTHORIZED_ATTRIBUTE, authorized);
    
        if (authorized) {
            // Put user data into model for display
            data.put("email", client.getEmail(preserver));
            data.put("userId", client.getUserId(preserver));
        } else {
            // Put authURL into model for creation of login button
            data.put("authURL", client.getAuthorizationRequestUrl(preserver));
        }
    
        // return model and view for template rendering
        return prepareModelAndView(req.session(), data);
    }

    You can see that, if the user is authorized, this info is passed to the HashMap, and the .getEmail() and .getUserId() methods are used to return the user data to the model and template. The isAuthorized method also returns an access token.

    If the user is not authorized, the .getAuthorizationRequestUrl() method is used to pass authURL to the template (this passes the client_id and redirect_uri to the authentication server).

    Important Note on User Management

    In terms of managing your users, it is important to note that, in the process of providing a secure login solution, the service has also registered your users with a confirmed user email and user ID. Once a user has been authenticated it is possible to make use of the above .getEmail() and .getUserID methods to return string values.

    This removes a considerable amount of pain from the process of managing users and databases!

    For example, if you have a SQL user database and you want to make a check to see if a user is present, or needs added as a new user, then it is possible to make use of the above methods.

    Another example would be if you want to present a web form to capture more information that is needed to provide a user with access to your product features. Here you can use the above methods to prepopulate the ID and email address field, and you do not need to initiate a 'verify by email' process, as this has already been done by the service.

    Then at the callback handler / redirect url endpoint ("/login") the .validateAuthorization() method is used to check if a user's login attempt has been successful and send a success message if so:

    // Callback handler - process callback from login process
    private static ModelAndView handleLoginRequest(MiraclClient client, Request req, Response resp) {
        // Construct session wrapper for Miracl
        final MiraclSparkSessionWrapper preserver = new MiraclSparkSessionWrapper(req.session());
    
        // Model data for template
        Map<String, Object> data = new HashMap<>();
    
        // Request validation of authorization data, retrieving token
        final String token = client.validateAuthorization(preserver, req.queryString());
    
        if (token == null) {
            return failLogin(client, preserver, req, resp);
        }
    
        // Validate the JWT you received
        try {
            JwtValidator validator = new JwtValidator("RS256");
            validator.validateToken(token);
        } catch (MiraclException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
            return failLogin(client, preserver, req, resp);
        }
    
        // Prepare model and view before setting message
        final ModelAndView modelAndView = prepareModelAndView(req.session(), data);
        // Show message about successful log in
        flashMessage(req.session(), "success", "Successfully logged in");
        // and redirect back to start
        resp.redirect(ROOT_URL);
        // return model and view for template rendering
        return modelAndView;
    }

    If the login attempt fails another ModelAndView is prepared and a danger message along with retry and the authURL is sent:

    // Prepare a ModelAndView for returning whenever logging in failed
    private static ModelAndView failLogin(MiraclClient client, MiraclSparkSessionWrapper preserver, Request req,
            Response resp) {
    
        // Model data for template
        Map<String, Object> data = new HashMap<>();
    
        // Show message about fail to log in
        flashMessage(req.session(), "danger", "Login failed!");
    
        data.put(RETRY_ATTRIBUTE, true);
        data.put("authURL", client.getAuthorizationRequestUrl(preserver));
    
        // return model and view for template rendering
        return prepareModelAndView(req.session(), data);
    }

    A refresh handler ("/refresh") can then be used, which clears userInfo from the session, but leaves the access token unchanged. This means that user info can be requested without performing a fresh authorization. And a logout ("/logout") handler can log the user out and redirect them to the root url:

    // Refresh handler - refresh user data and redirect back to beginning
    private static String handleRefreshRequest(MiraclClient client, Request req, Response resp) {
        // Construct session wrapper for Miracl
        final MiraclSparkSessionWrapper preserver = new MiraclSparkSessionWrapper(req.session());
        // Clear user info. It will be re-retrieved when requested
        client.clearUserInfo(preserver);
        // Redirect back to start
        resp.redirect(ROOT_URL);
        // Return nothing to render as redirect already have prepared response
        return "";
    }
    // Logout handler - log out user and redirect back to beginning
    private static String handleLogoutRequest(MiraclClient client, Request req, Response resp) {
        // Constructing session wrapper for Miracl
        final MiraclSparkSessionWrapper preserver = new MiraclSparkSessionWrapper(req.session());
        // Clear user info and related session entries
        client.clearUserInfoAndSession(preserver);
        // Notify user about log out
        flashMessage(req.session(), "info", "User logged out!");
        // Redirect back to start
        resp.redirect(ROOT_URL);
        // Return nothing to render as redirect already have prepared response
        return "";
    }    
  4. Read the configuration data from miracl.json

    An input stream reader can be set up to read the configuration variables:

    // Read the JSON-formatted configuration data stored in miracl.json.
    private static JsonObject readConfiguration() throws IOException {
        String confFilename = "miracl.json";
        // Read configuration from miracl.json file for Miracl client
        // construction
        try (InputStream configStream = SparkSample.class.getClassLoader().getResourceAsStream(confFilename)) {
            return Json.parse(new InputStreamReader(configStream)).asObject();
        }
    }
    
    public static void main(String[] args) throws IOException {
        final JsonObject config = readConfiguration();
    
        String clientId = config.get("client_id").asString();
        String secret = config.get("secret").asString();
        String redirectUri = config.get("redirect_uri").asString();
        int serverPort = config.getInt("serverPort", DEFAULT_HOST);
    
        boolean useProxy = config.getBoolean("use_proxy", false);
        String proxyHost = config.getString("proxy_host", DEFAULT_PROXY_HOST);
        String proxyPort = config.getString("proxy_port", DEFAULT_PROXY_PORT);
  5. Final Steps

    Finally, the template engine is prepared, the port is set and a new MiraclClient is initiated using the variables retrieved from miracl.json:

    // Prepare template engine
    final PebbleEngine pebbleEngine = new PebbleEngine(new ResourcesLoader());
    pebbleEngine.setStrictVariables(true);
    final TemplateEngine templateEngine = new PebbleTemplateEngine(pebbleEngine);
    
    // Set Spark port
    port(serverPort);
    
    // Prepare Miracl client instance
    MiraclClient miracl = new MiraclClient(clientId, secret, redirectUri);
    
    if (useProxy) {
        MiraclClient.useProxy(proxyHost, proxyPort);
    }
    
    get(ROOT_URL, (req, res) -> handleMainRequest(miracl, req), templateEngine);
    get("/login", (req, res) -> handleLoginRequest(miracl, req, res), templateEngine);
    get("/refresh", (req, res) -> handleRefreshRequest(miracl, req, res));
    get("/logout", (req, res) -> handleLogoutRequest(miracl, req, res));

    As you can see the request handlers are also allocated to their endpoints.

Setup Notes

Current sdk version is 0.1-SNAPSHOT. Replace VERSION with needed version in examples.

To use maas-sdk with a Maven project, use:

<dependency>
  <groupId>com.miracl</groupId>
  <artifactId>maas-sdk</artifactId>
  <version>VERSION</version>
</dependency>

For a Gradle project:

dependencies {
    compile 'com.miracl:maas-sdk:VERSION'
}

All commands in this document are for Linux/MacOS. For Windows, replace ./gradlew with gradlew.bat.

Local Installation

Use ./gradlew maas-sdk:publishToMavenLocal to compile SDK and install it as an artifact to the local Maven repository. maas-sdk directory can also be used as a subproject in a Gradle project.

Documentation generation

To generate JavaDoc, use ./gradlew maas-sdk:javadoc. The result can be found in maas-sdk/build/docs.

Top