HTTP-based custom authentication

Custom authentication is a form of API authentication and is based on an HTTP protocol.

Overview

The custom identity provider authenticates a user by sending challenges to the client. However, custom identity providers do not communicate directly with clients. They send challenges and receive responses to the challenges by means of the IBM MobileFirst™ Platform Foundation authorization server. The process of authenticating a user typically involves a dialog with the client and consists of several requests and responses.
The HTTP protocol that is used in custom authentication is between IBM MobileFirst Platform Foundation authorization server and custom code. The custom code can be written in any language and can be protected or unprotected. If you choose to protect it, implement protection by using the default scope. For example, the Node.js sample on this page uses:
passport.authenticate('mobilefirst-strategy', {
    session : false
}
to protect the startAuthorization resource.

To implement custom identity provider, you must expose the following two HTTP endpoints:

  • /startAuthorization endpoint. The headers that it receives are those which the client sends to the IBM MobileFirst Platform Foundation and which the server then adds to the request body. For example, the Node.js sample on this page gets the headers by using the following code:
    function(req, res) {
        var returnedJSON = startAuthorization(req.body.headers);
        res.json(returnedJSON);
    }
  • /handleChallengeAnswer endpoint. The headers that it receives are those which are received at the /startAuthorization endpoint, with the addition of an answer to a challenge and a state ID. This HTTP endpoint checks the challenge and returns success, challenge, or failure.

The custom identity provider may store some state related to the authentication dialog. An example use case is multi-step authentication, where the custom identity provider needs to store the result of the first authentication step when proceeding to the next step. In this case, the dialog between IBM MobileFirst Platform Foundation authorization server and the custom identity provider becomes stateful. The custom identity provider assigns a state ID, which is sent in the response. The IBM MobileFirst Platform Foundation authorization server passes the state ID in subsequent requests that belong to the client authentication process.

Headers are sent in the body because it is the header from the client request. Sending headers from the client in the body from authorization server to the custom identity provider will prevent conflicts in header names. In case you need to save state between calls to startAuthorization and handleChallengeAnswer, use the optional stateId property in the challenge answer. The custom identity provider and the protected resource may be implemented on the same server or on different servers.

The realm is always in the URL path, just before the endpoint API, as shown in the following examples:

  • http://myhost:myport/custom_realm/startAuthorization
  • http://myhost:myport/custom_realm/handleChallengeAnswer

Both HTTP endpoints can return three types of JSON response:

  • challenge
    { 
    status: "challenge", challenge: 
        { your custom JSON challenge }, 
        stateId : custom state id //state id is optional 
    }
  • success
    {
     status: "success", userIdentity: 
        {userName: userName, displayName: displayName, attributes: { your optional custom JSON attributes } }
    }
  • failure
    { status: "failure" } 

Samples

Let's say you want to protect an HTTP resource that is defined by the URL /protected_resource, with the scope "customAuthRealm_1 customAuthRealm_2". Perform the following steps:
  1. Create an IBM MobileFirst Platform Foundation project.
  2. In the project, define two realms and their login modules in the authenticationConfig.xml file.
    Note: The className property that is shown both in the realm and login module for this example must be used as is, in all cases.
    <realms>
        <realm name="customAuthRealm_1" loginModule="customAuthLoginModule_1">
            <className>com.worklight.core.auth.ext.CustomIdentityAuthenticator</className>    
            <parameter name="providerUrl" value="http://localhost:3000"/>
        </realm>
    
        <realm name="customAuthRealm_2" loginModule="customAuthLoginModule_2">
            <className>com.worklight.core.auth.ext.CustomIdentityAuthenticator</className>    
            <parameter name="providerUrl" value="http://localhost:3000"/>
        </realm> 
    </realms>
    <loginModules>
        <loginModule name="customAuthLoginModule_1">
            <className>com.worklight.core.auth.ext.CustomIdentityLoginModule</className>
        </loginModule>
    
        <loginModule name="customAuthLoginModule_2">
            <className>com.worklight.core.auth.ext.CustomIdentityLoginModule</className>
        </loginModule>
    </loginModules>
  3. Configure the realm to be a user identity realm: To get the user identity in Java™ adapter, Node.js, or TAI resource, you must specify them in the application-descriptor.xml file of the application, in the user identity realms field.
    Note: User identity realms are comma separated. The order of the realms dictates the selected user identity. If the list is empty, the ID token contains no identity information.
  4. Now proceed to the Node.js or Java JAX-RS sample code in the next sections to see a full implementation of the solution.
Node.JS sample

The following sample code demonstrates how to implement the startAuthorization and handleChallengeAnswer endpoints in Node.js.

// Call the protected_nodejs_resource
    app.get('/protected_nodejs_resource',
      passport.authenticate('mobilefirst-strategy', {session : false, scope: "customAuthRealm_1 customAuthRealm_2" }), function(req, res) {
       res.status(200).send("Hello capella from node.js")); 
    });

//Implement custom authentication for customAuthRealm_1
    app.post('/customAuthRealm_1/startAuthorization',
        passport.authenticate('mobilefirst-strategy', {
            session : false
        }),
        function(req, res) {
            var returnedJSON = startAuthorization(req.body.headers);
            res.json(returnedJSON);
    });

    app.post('/customAuthRealm_1/handleChallengeAnswer',
            passport.authenticate('mobilefirst-strategy', {
                session : false
            }),     function(req, res) {
            var returnedJSON = handleChallengeAnswer(req.body.headers, req.body.stateId, req.body.challengeAnswer);
            res.json(returnedJSON);
    });


    var startAuthorization = function(headers) {
        return { 
            status: "challenge",
                challenge: {
                    message: "missing_credentials"
                }, 
            stateId : "my_custom_state_id"
        };  
    };


    var handleChallengeAnswer = function(headers, stateId, challengeAnswer) {
        if (challengeAnswer && users[challengeAnswer.userName] && challengeAnswer.password === users[challengeAnswer.userName].password) {
            return {
                status: "success",
                userIdentity: {
                    userName: challengeAnswer.userName,
                    displayName: users[challengeAnswer.userName].displayName,
                    attributes : {"customAttr1":"customValue1", "customAttr2":"customValue2"}

                }
            };
        } else {
            return {
                status: "failure"
            }
        }
    };
Java JAX-RS sample

The following JAX-RS sample is implemented as a Java REST adapter. The startAuthorization and handleChallengeAnswer endpoints have been implemented in Java.

// Call the protected_java_resource
@GET
@Path("/protected_java_resource")
@Produces("application/json")
@OAuthSecurity (scope="customAuthRealm_1 customAuthRealm_2")
public JSONObject helloCapella(){
    JSONObject hello = new JSONObject();
    hello.put("hello", "Hello Capella");
    return hello;
}



@POST
    @Consumes ("application/json")
    @Path("/customAuthRealm_1/startAuthorization")
    @Produces(MediaType.APPLICATION_JSON)
    public JSONObject startAuthorization(String payload) throws Exception{
        logger.info(payload);
        JSONObject returnJson = (JSONObject) JSON.parse(CHALLENGE_JSON);
        return returnJson;
    }
 
 
   @POST
   @Consumes ("application/json")
   @Path("/customAuthRealm_1/handleChallengeAnswer")
   @Produces(MediaType.APPLICATION_JSON)
    public JSONObject handleChallengeAnswer(String payload) throws Exception{
        JSONObject userStoreJson = (JSONObject) JSON.parse(USER_STORE_JSON);
        JSONObject failedResponseJson = (JSONObject) JSON.parse(FAILURE_JSON);
        
        if(payload == null || payload.isEmpty()) {
            return failedResponseJson;
        }
        JSONObject payloadJson = (JSONObject) JSON.parse(payload);
        JSONObject challengeAnswer = (JSONObject) payloadJson.get("challengeAnswer");
        
        if (challengeAnswer == null ) {
            return failedResponseJson;
        }
        
        String userName = (String) challengeAnswer.get("userName");
        String password = (String) challengeAnswer.get("password");
        
        if (userName == null || userName.isEmpty() || password == null || password.isEmpty()) {
            return failedResponseJson;
        }
        
        if (userStoreJson.containsKey(userName)) {
            JSONObject userInfoJson = (JSONObject) userStoreJson.get(userName);
            String userPassword = (String) userInfoJson.get("password");
            String userDisplayName = (String) userInfoJson.get("displayName");
            
            if (password.equals(userPassword)) {
                JSONObject returnJson = new JSONObject();
                JSONObject userIdentityJson = new JSONObject();
                userIdentityJson.put("userName", userName);
                userIdentityJson.put("displayName", userDisplayName);
                JSONObject customAttributes = new JSONObject();
                customAttributes.put("custom1Key", "custom1Value");
                customAttributes.put("custom2Key", "custom2Value");
                userIdentityJson.put("attributes", customAttributes);
                returnJson.put("status", "success");
                returnJson.put("userIdentity", userIdentityJson);
                return returnJson;
            }            
        }
        
        return failedResponseJson;
    }

For more information

More techniques for implementing custom authentication

IBM MobileFirst Platform Foundation provides other custom authentication mechanisms: