Implementing Authentication for REST API calls from JET Applications embedded in ADF or WebCenter Portal using JSON Web Token (JWT) image 122

Implementing Authentication for REST API calls from JET Applications embedded in ADF or WebCenter Portal using JSON Web Token (JWT)

imageThe situation discussed in this article is as follows: a rich client web application (JavaScript based, could be created with Oracle JET or based on Angular/Vue/React/Ember/…) is embedded in an ADF or WebCenter Portal application. Users are authenticated in that application through a regular login procedure that leverages the OPSS (Oracle Platform Security Service) in WebLogic, authenticating against an LDAP directory or another type of security provider. The embedded rich web application makes calls to REST APIs. These APIs enforce authentication and authorization – to prevent rogue calls. Note: these APIs have to be accessible from wherever the users are working with the ADF or WebCenter Portal application.

This article describes how the authenticated HTTP Session context in ADF – where we have the security context with authenticated principal with subjects and roles – can be leveraged to generate a secure token that can be passed to the embedded client web application and subsequently used by that application to make calls to REST APIs that can verify through that token that an authenticated user is making the call. The REST API can also extract relevant information from the token – such as the user’s identity, permissions or entitlements and custom attributes. The token could also be used by the REST API to retrieve additional information about the user and his or her session context.

Note: if calls are made to REST APIs that are deployed as part of the enterprise application (same EAR file) that contains the ADF or WebCenter Portal application, then the session cookie mechanism ensures that the REST API handles the request in the same [authenticated] session context. In that case, there is no need for a token exchange.

 

Steps described in this article:

  1. Create a managed session bean that can be called upon to generate the JWT Token
  2. Include the token from this session bean in the URL that loads the client web application into the IFrame embedded in the ADF application
  3. Store the token in the web client
  4. Append the token to REST API calls made from the client application
  5. Receive and inspect the token inside the REST API to ensure the authenticated status of the user; extract additional information from the token

As a starting point, we will assume an ADF application for which security has been configured, forcing users accessing the application to login by providing user credentials.

The complete application in a working – though somewhat crude – form with code that absolutely not standards compliant nor production ready can be found on GitHub: https://github.com/lucasjellema/adf-embedded-js-client-token-rest.

 

Create a managed session bean that can be called upon to generate the JWT Token

I will use a managed bean to generate the JWT Token, either in session scope (to reuse the token) or in request scope (to generate fresh tokens on demand) .

JDeveloper and WebLogic both ship with libraries that support the generation of JWT Tokens. In a Fusion Web Application the correct libraries are present by default. Anyone of these libraries will suffice:

image

I create a new class as the Token Generator:

package nl.amis.portal.view;

import java.util.Date;

import javax.faces.bean.SessionScoped;
import javax.faces.bean.ManagedBean;

import oracle.adf.share.ADFContext;
import oracle.adf.share.security.SecurityContext;

import oracle.security.restsec.jwt.JwtToken;
import java.util.HashMap;
import java.util.Map;
@ManagedBean
@SessionScoped
public class SessionTokenGenerator {
    
    private String token = ";";
    private final String secretKey = "SpecialKeyKeepSecret";
    public SessionTokenGenerator() {
        super();
        ADFContext adfCtx = ADFContext.getCurrent();  
        SecurityContext secCntx = adfCtx.getSecurityContext();  
        String user = secCntx.getUserPrincipal().getName();  
        String _user = secCntx.getUserName();  
        try {
            String jwt = generateJWT(user, "some parameter value - just because we can", _user, secretKey);
            this.token = jwt;
        } catch (Exception e) {
        }
    }

    public String generateJWT(String subject, String extraParam, String extraParam2, String myKey) throws Exception {           
           String result = null;        
           JwtToken jwtToken = new JwtToken();
           //Fill in all the parameters- algorithm, issuer, expiry time, other claims etc
           jwtToken.setAlgorithm(JwtToken.SIGN_ALGORITHM.HS512.toString());
           jwtToken.setType(JwtToken.JWT);
           jwtToken.setClaimParameter("ExtraParam", extraParam);
           jwtToken.setClaimParameter("ExtraParam2", extraParam2);
           long nowMillis = System.currentTimeMillis();
           Date now = new Date(nowMillis);
           jwtToken.setIssueTime(now);
           // expiry = 5 minutes - only for demo purposes; in real life, several hours - equivalent to HttpSession Timeout in web.xml - seems more realistic
           jwtToken.setExpiryTime(new Date(nowMillis + 5*60*1000));
                                           jwtToken.setSubject(subject);
                                           jwtToken.setIssuer("ADF_JET_REST_APP");
           // Get the private key and sign the token with a secret key or a private key
           result = jwtToken.signAndSerialize(myKey.getBytes());
           return result;
       }

    public String getToken() {
        return token;
    }
}

Embed the Web Client Application

The ADF Application consists of main page – index.jsf – that contains a region binding a taskflow that in turn contains a page fragment (client-app.jsff) that consists of a panelStretchLayout that contains an inline frame (rendered as an IFrame) that loads the web client application.

image

The JWT token (just a long string) has to be included in the URL that loads the client web application into the IFrame. This is easily done by adding an EL Expression in the URL property:

 <af:inlineFrame source="client-web-app/index.xhtml?token=#{sessionTokenGenerator.token}"
                            id="if1" sizing="preferred" styleClass="AFStretchWidth"/>

 

Store the token in the web client

When the client application is loaded, the token can be retrieved from the query parameters. An extremely naive implementation uses an onLoad event trigger on the body object to call a function that reads the token from the query parameters on the window.location.href object and stores it in the session storage:

function getQueryParams() {
    token = getParameterByName('token');
    if (token) {
        document.getElementById('content').innerHTML += '<br>Token was received and saved in the client for future REST calls';
        // Save token to sessionStorage
        sessionStorage.setItem('portalToken', token);    }
    else 
        document.getElementById('content').innerHTML += '<br>Token was NOT received; you will not be able to use this web application in a meaningful way';
}

function getParameterByName(name, url) {
    if (!url)
        url = window.location.href;
    var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), results = regex.exec(url);
    if (!results)
        return null;
    if (!results[2])
        return '';
    return decodeURIComponent(results[2].replace(/\+/g, " "));
}

If we wanted to so do, we can parse the token in the client and extract information from it – using a function like this one:

 

function parseJwt(token) {
    var base64Url = token.split('.')[1];
    var base64 = base64Url.replace('-', '+').replace('_', '/');
    return JSON.parse(window.atob(base64));
};

 

Append the token to REST API calls made from the client application

Whenever the client application makes REST API calls, it should include the JWT token in an HTTP Header. Here is example code for making an AJAX style REST API call – with the token included in the Authorization header:

function callServlet() {
    var portalToken = sessionStorage.getItem('portalToken');
    // in this example the REST API runs on the same host and port as the ADF Application; that need not be the case - the following URL is also a good example: 
    // var targetURL = 'http://some.otherhost.com:8123/api/things';
    var targetURL = '/ADF_JET_REST-ViewController-context-root/restproxy/rest-api/person';
    var xhr = new XMLHttpRequest();
    xhr.open('GET', targetURL)
    xhr.setRequestHeader("Authorization", "Bearer " +  portalToken);
    xhr.onload = function () {
        if (xhr.status === 200) {
            alert('Response ' + xhr.responseText);
        }
        else {
            alert('Request failed.  Returned status of ' + xhr.status);
        }
    };
    xhr.send();
}

 

Receive and inspect the token inside the REST API to ensure the authenticated status of the user

Depending on how the REST API is implemented – for example Java with JAX-RS, Node with Express, Python, PHP, C# – the inspection of the token will take a place in a slightly different way.

With JAX-RS based REST APIs running on a Java EE Web Server, one possible approach to inspection of the token is using a ServletFilter. This filter can front the JAX-RS service and stay completely independent of it. By mapping the Servlet Filter to all URL paths on which REST APIs can be accessed, we ensure that these REST APIs can only be accessed by requests that contain valid tokens.

A more simplistic, less elegant approach is to just make the inspection of the token an explicit part of the REST API. The Java code required for both approaches is very similar. Here is the code I used in a simple servlet that sits between the incoming REST API request and the actual REST API as a proxy that verifies the token, does the CORS headers and does the routing:

 

package nl.amis.portal.view;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;

import java.net.HttpURLConnection;
import java.net.URL;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

import javax.ws.rs.core.HttpHeaders;

import oracle.adf.share.ADFContext;
import oracle.adf.share.security.SecurityContext;

import java.util.Date;

import java.util.Map;

import oracle.security.restsec.jwt.JwtException;
import oracle.security.restsec.jwt.JwtToken;
import oracle.security.restsec.jwt.VerifyException;


@WebServlet(name = "RESTProxy", urlPatterns = { "/restproxy/*" })
public class RESTProxy extends HttpServlet {
    private static final String CONTENT_TYPE = "application/json; charset=UTF-8";
    private final String secretKey = "SpecialKeyKeepSecret";


    public void init(ServletConfig config) throws ServletException {
        super.init(config);
    }

    private TokenDetails validateToken(HttpServletRequest request) {
        TokenDetails td = new TokenDetails();
        try {
            boolean tokenAccepted = false;
            boolean tokenValid = false;
            // 1. check if request contains token

            // Get the HTTP Authorization header from the request
            String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);

            // Extract the token from the HTTP Authorization header
            String tokenString = authorizationHeader.substring("Bearer".length()).trim();

            String jwtToken = "";
            String issuer = "";
            td.setIsTokenPresent(true);

            try {
                JwtToken token = new JwtToken(tokenString);
                // verify whether token was signed with my key
                boolean result = token.verify(secretKey.getBytes());
                if (!result) {
                    td.addMotivation("Token was not signed with correct key");
                } else {
                    td.setIsTokenVerified(true);
                    td.setJwtTokenString(tokenString);
                    tokenAccepted = false;
                }

                // Validate the issued and expiry time stamp.
                if (token.getExpiryTime().after(new Date())) {
                    jwtToken = tokenString;
                    tokenValid = true;
                    td.setIsTokenFresh(true);
                } else {
                    td.addMotivation("Token has expired");
                }

                // Get the issuer from the token
                issuer = token.getIssuer();
                // possibly validate/verify the issuer as well
                
                td.setIsTokenAccepted(td.isIsTokenPresent() && td.isIsTokenFresh() && td.isIsTokenVerified());
                return td;

            } catch (JwtException e) {
                td.addMotivation("No valid token was found in request");

            } catch (VerifyException e) {
                td.addMotivation("Token was not verified (not signed using correct key");

            }
        } catch (Exception e) {
            td.addMotivation("No valid token was found in request");
        }
        return td;
    }

    private void addCORS(HttpServletResponse response) {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        TokenDetails td = validateToken(request);
        if (!td.isIsTokenAccepted()) {
            response.setContentType(CONTENT_TYPE);
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.addHeader("Refusal-Motivation", td.getMotivation());
            addCORS(response);
            response.getOutputStream().close();
        } else {

            // optionally parse token, extract details for user

            // get URL path for REST call
            String pathInfo = request.getPathInfo();

            // redirect the API call/ call API and return result

            URL url = new URL("http://127.0.0.1:7101/RESTBackend/resources" + pathInfo);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setRequestProperty("Accept", "application/json");

            if (conn.getResponseCode() != 200) {
                throw new RuntimeException("Failed : HTTP error code : " + conn.getResponseCode());
            }

            BufferedReader br = new BufferedReader(new InputStreamReader((conn.getInputStream())));


            response.setContentType(CONTENT_TYPE);
            // see http://javahonk.com/enable-cors-cross-origin-requests-restful-web-service/
            addCORS(response);

            response.setStatus(conn.getResponseCode());
            RESTProxy.copyStream(conn.getInputStream(), response.getOutputStream());
            response.getOutputStream().close();
        } // token valid so continue

    }


    public static void copyStream(InputStream input, OutputStream output) throws IOException {
        byte[] buffer = new byte[1024]; // Adjust if you want
        int bytesRead;
        while ((bytesRead = input.read(buffer)) != -1) {
            output.write(buffer, 0, bytesRead);
        }
    }

   private class TokenDetails {
        private String jwtTokenString;
        private String motivation;

        private boolean isJSessionEstablished; // Http Session could be reestablished
        private boolean isTokenVerified; // signed with correct key
        private boolean isTokenFresh; // not expired yet
        private boolean isTokenPresent; // is there a token at all
        private boolean isTokenValid; // can it be parsed
        private boolean isTokenIssued; // issued by a trusted token issuer

        private boolean isTokenAccepted = false; // overall conclusion

        ... plus getters and setters

}

 

Running the ADF Application with the Embedded Client Web Application

When  accessing the ADF application in the browser, we are prompted with the login dialog:

image

After successful authentication, the ADF Web Application renders its first page. This includes the Taskflow that contains the Inline Frame that loads the client web application using a URL that contains the token.

image

When the link is clicked in the client web application, the AJAX call is made – the call that has the token included in a Authorization Request header. The first time we make the call, the result is shown as returned from the REST API

image

However, a second call after more than 5 minutes fails:

image

Upon closer inspection of the request, we find the reason: the token has expired:

image

The token based authentication has done a good job.

Similarly, when we try to access the REST API directly – we need to have a valid token or we are unsuccessful:

image

 

Inspect token in Node based REST API

REST APIs can be implemented in various technologies. One popular option is Node – using server side JavaScript. Node applications are perfectly capable of doing inspection of JWT tokens – verifying their validity and extracting information from the token. A simple example is shown here – using the NPM module jsonwebtoken:

 

// Handle REST requests (POST and GET) for departments
var express = require('express') //npm install express
  , bodyParser = require('body-parser') // npm install body-parser
  , http = require('http')
  ;

var jwt = require('jsonwebtoken');
var PORT = process.env.PORT || 8123;


const app = express()
  .use(bodyParser.urlencoded({ extended: true }))
  ;

const server = http.createServer(app);

var allowCrossDomain = function (req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  res.header('Access-Control-Allow-Credentials', true);
  res.header("Access-Control-Allow-Headers", "Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Authorization, Access-Control-Request-Method, Access-Control-Request-Headers");
  next();
}

app.use(allowCrossDomain);

server.listen(PORT, function listening() {
  console.log('Listening on %d', server.address().port);
});

app.get('/api/things', function (req, res) {
  // check header or url parameters or post parameters for token
  var token = req.body.token || req.query.token || req.headers['x-access-token'];
  if (req.headers && req.headers.authorization) {
    var parts = req.headers.authorization.split(' ');
    if (parts.length === 2 && parts[0] === 'Bearer') {
      // two tokens sent in the request
      if (token) {
        error = true;
      }
      token = parts[1];
    }
  }
  var decoded = jwt.decode(token);

  // get the decoded payload and header
  var decoded = jwt.decode(token, { complete: true });
  var subject = decoded.payload.sub;
  var issuer = decoded.payload.iss;

  // verify key
  var myKey = "SpecialKeyKeepSecret";
  var rejectionMotivation;
  var tokenValid = false;

  jwt.verify(token, myKey, function (err, decoded) {
    if (err) {
      rejectionMotivation = err.name + " - " + err.message;
    } else {
      tokenValid = true;
    }
  });


  if (!tokenValid) {
    res.status(403);
    res.header("Refusal-Motivation", rejectionMotivation);
    res.end();
  } else {
      // do the thing the REST API is supposed to do
      var things = { "collection": [{ "name": "bicycle" }, { "name": "table" }, { "name": "car" }] }

      res.status(200);
      res.header('Content-Type', 'application/json');
      res.end(JSON.stringify(things));
  }
  }
});

One Response

  1. Sudhakar March 17, 2018