Persona implementation using Java, the whole story

Blog
21/12/13
Lluis Turró Cutiller
27.598
3
java persona

I decided to publish Persona implementation mainly because wasn't as easy as explained in Persona site. Also because is lacking of Java code, at least, Java code with no-so-much dependencies.

Follow the instructions found in Quick Setup at Persona site. Notice that the instructions provide best practices for including Persona dependencies. When finished, come back here and prepare for Persona implemented in your Java code.

Lets begin with the easy part, the Java Script code. This is the persona.js file. The example uses JQuery.

/*stands for context path on servlets nomenclature*/
var webRoot = ""; 
/*persona wants to know who is signed in*/
var currentMail = null; 
/*for app servers running on different ports*/
var webPort = 80; 
/*did user signed in without persona*/
var internalSignIn = false;
/*should we reload current page */
var reloadSignIn = false; 

$(document).ready(function() {
  loadElephant();
  if(!internalSignIn) {
    navigator.id.watch({
      loggedInUser: currentMail,
      onlogin: function(assertion) {
        $.ajax({
          type: 'POST',
          url: webRoot + '/auth/login', 
          port: webPort,
          data: {assertion: assertion},
          success: function(res, status, xhr) { 
            if(reloadSignIn) { 
              window.location.href = window.location.href; 
            }
          },
          error: function(xhr, status, err) {
            navigator.id.logout();
          }
        });
      },
      onlogout: function() {
        $.ajax({
          type: 'POST',
          url: webRoot + '/auth/logout', 
          port: webPort,
          success: function(res, status, xhr) { 
            window.location.href = window.location.href; 
          },
          error: function(xhr, status, err) {  }
        });
      }
    });
  }
});

Notice the use of some variables that will make your coding more useful in the long term. OK, now we dive into their use and how to get them initialized:

I use an internal call to Elephant libraries, which is something you, probably, would not do. The secret is to generate the method loadElephant(), or however you want it to be named, within the HTML page body. And give variables their current values. This way, variables are declare first as global, then the page loads in browser, the method loadElephant() got its way in the created HTML and finally, when document is completely loaded, navigator.id.watch() executes.

The loadElephant() method will look something like this (remember you are responsible of creating this method from actual values):

<script type='text/javascript'>
  function elephantLoad() {
    webRoot = '';
    webPort = 80;
    currentMail = 'support@turro.org';
  }
</script>

From the original code at Persona site, I also changed the way of reloading current page. Despite JavaScript specification with regards to window.location.reload() method, I found that page reloading is much slower than when you simply reasign href. I took this approach.

Global variables and their use

  • webRoot is self explanatory. Points to servlet context path. To make your code portable you need to deal with application paths on web servers and JavaScript lack of interest.
  • webPort is much the same than webRoot. Application servers use to vary quite a lot the TCP/IP port serving pages.
  • currentMail will tell Persona who has already signed in that session. More on this in Java section.
  • internalSignIn will by-pass persona calling and I use it to signal that user has chosen the internal system.
  • reloadSignIn tells no to reload page contents. Since Persona will sign you in, if you said so, after page loading, reloadSignIn stops reloading contents for those pages where nothing change regardless a user is in or not. Lets say, the landing page compared to the user area.

The Persona servlet, hands on work

Here we need to choose which way we'll use to connect to secured Persona site. My election is HttpClient from Apache Foundation. Sound, reliable and full browser-like support. In order to load body request cleanly, this code depends also on Apache Commons IO.

I used servlets annotations to point to which calling paths this servlet will respond. If you use an old servlet container, then configure web.xml file. Here is the code:

@WebServlet({"/auth/login", "/auth/logout"})
public class PersonaServlet extends HttpServlet {

  private static final String personaVerify = "https://verifier.login.persona.org/verify";

  private HttpClient httpClient;

  private class PersonaVerify {
    public String audience;
    public long expires;
    public String issuer;
    public String email;
    public String status;
  }
  
  @Override
  public void init(ServletConfig config) throws ServletException {
    super.init(config);
    httpClient = getHttpClient();
  }

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response)
          throws ServletException, IOException {
    String requestURI = request.getRequestURI();
    Logger.getLogger(PersonaServlet.class.getName()).log(Level.INFO, requestURI);
    if (requestURI.equalsIgnoreCase(request.getContextPath() + "/auth/login")) {
      handleLoginRequest(request, response);
    } else if (requestURI.equalsIgnoreCase(request.getContextPath() + "/auth/logout")) {
      handleLogoutRequest(request, response);
    } else {
      errorMessage(response, HttpServletResponse.SC_NOT_FOUND, "Not found");
    }
  }

  final String getAssertion(HttpServletRequest request, HttpServletResponse response)
          throws IOException {

    StringWriter writer = new StringWriter();
    IOUtils.copy(request.getInputStream(), writer, Consts.UTF_8.toString());
    
    final String[] parameters = writer.toString().split("=");

    if (parameters.length != 2 || !parameters[0].equals("assertion")) {
      errorMessage(response, HttpServletResponse.SC_BAD_REQUEST, "Malformed assertion");
      return null;
    }

    return parameters[1];
  }

  final String verifyAssertion(final String requestAssertion, HttpServletRequest request,
          HttpServletResponse response) throws IOException {
    StringWriter writer = new StringWriter();

    HttpPost httpPost = new HttpPost(personaVerify);
    List nvps = new ArrayList<>();
    nvps.add(new BasicNameValuePair("assertion", requestAssertion));
    nvps.add(new BasicNameValuePair("audience", getSiteAudience(request)));
    httpPost.setEntity(new UrlEncodedFormEntity(nvps, Consts.UTF_8));
    try (CloseableHttpResponse closableResponse = (CloseableHttpResponse) httpClient.execute(httpPost)) {
      HttpEntity entity = closableResponse.getEntity();
      IOUtils.copy(entity.getContent(), writer, Consts.UTF_8.toString());
      EntityUtils.consume(entity);
    }

    return writer.toString();
  }

  private void handleLoginRequest(HttpServletRequest request, HttpServletResponse response)
          throws IOException {
    final String requestAssertion = getAssertion(request, response);
    if (requestAssertion != null) {
      //Logger.getLogger(PersonaServlet.class.getName()).log(Level.INFO, "Assertion:" + requestAssertion);
      final String verificationResponse = verifyAssertion(requestAssertion, request, response);
      if (verificationResponse != null) {
        //Logger.getLogger(PersonaServlet.class.getName()).log(Level.INFO, verificationResponse);
        PersonaVerify verification = new Gson().fromJson(verificationResponse, PersonaVerify.class);
        if(verification != null && verification.email != null && "okay".equals(verification.status)) {
          /* Here you do your own home work. That's sign in verification.email */
          if(/* signed in */(verification.email)) {
            okMessage(response, verification.email);
          } else {
            errorMessage(response, HttpServletResponse.SC_BAD_REQUEST, "Log failed");
          }
        } else {
          errorMessage(response, HttpServletResponse.SC_BAD_REQUEST, "Log failed");
        }
      } else {
        errorMessage(response, HttpServletResponse.SC_BAD_REQUEST, "Log failed");
      }
    } else {
      errorMessage(response, HttpServletResponse.SC_BAD_REQUEST, "Log failed");
    }
  }

  private void handleLogoutRequest(HttpServletRequest request, HttpServletResponse response)
          throws IOException {
    /* Sign out current user */
    constructor.redirect("/");
    /* Invalidate this session */
    okMessage(response, "Logged out");
  }

  private String getSiteAudience(HttpServletRequest request) {
    // use a configuration file instead as explained in
    // https://developer.mozilla.org/en-US/Persona/Security_Considerations#Explicitly_specify_the_audience_parameter 
    return "http://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath();
  }

  private void errorMessage(HttpServletResponse response, int errorSC, String message) throws IOException {
    response.setStatus(errorSC);
    response.setContentType("text/plain");
    response.getOutputStream().write(message.getBytes(Consts.UTF_8));
  }

  private void okMessage(HttpServletResponse response, String message) throws IOException {
    response.setStatus(HttpServletResponse.SC_OK);
    response.setContentType("text/plain");
    response.getOutputStream().write(message.getBytes(Consts.UTF_8));
  }

  private CloseableHttpClient getHttpClient() {
    return HttpClients.custom()
            .setSslcontext(SSLContexts.createDefault())
            .build();
  }
  
}

What you need to know, what is not provided

I did not included the calls to a logging system, you should. For each case in handleLoginRequest at minimum. Otherwise you'll be lost in case of problems.

The calls to this servlet are transparent to you. You saw these calls in the JavaScript code.

Persona is called twice. First by client browser and finally by application server. Client will retrieve an assertion key from Persona, which was stored for the site audience you provided on previous calls. If no previous call was made, a new assertion is created. The method navigator.id.watch() passes the assertion to the application server, to your Persona servlet, in request's body. At this point, you need to call Persona in a secured connection and pass the assertion along with the audience. Persona will respond with a PersonaVerify structure containing the valid email.

That's all about Persona. Now is your decision waht to do with this email.

Security gaps your application may have

Since login systems use to be related to nickname/email versus password, changing to Persona only email may introduce some security gaps. Two might be allowing more than one email per user and duplicated emails.

Changes will be time spending, but Persona is worth trying. Good luck!

Comentaris

Anderson Chow 22/12/13
I use Spring, don't know Elephant. Could you provide an Spring version?
Lluís Turró Cutiller (Turró.Org) 23/12/13
The servlet should work correctly on Spring. In the example Elephant is present only by method name.

I'm planning to migrate Persona servlet to a filter which may reduce traffic when reloading and will become a more natural flow.
Sean McArthur 2/1/14 https://login.persona.org
Awesome! Thanks for sharing it here. Hopefully other Java server developers find it useful.