Enterprise Java

A Guide To Authenticating Users With Mozilla Persona

Having only twitter and facebook authentication so far, I decided to add Mozilla Persona to the list for my latest project (computoser, computer-generated music). Why?
 
 
 
 
 
 
 
 

  • I like trying new things
  • Storing passwords is a tough process, and even though I know how to do it, and even have most of the code written in another project, I don’t think that I should contribute to the landscape of every site requiring password authentication
  • Mozilla is an open foundation that has so far generated a lot of great products. Persona implements a BrowserID protocol that may be supported natively in browsers other than Firefox in the future (for now, you need to include a .js file)
  • 3rd party authentication has been attempted many times, and is a great thing, but isn’t mainstream for a couple of reasons. Being a bit different, Persona might succeed in becoming more popular.
  • This explanation by Mozilla makes sense

So, I started with the “Quick setup” guide. It is looks really easy. Way easier than OpenID or OAuth authentication – you don’t have to register anything anywhere, you don’t need 3rd party libraries for handling the verification on the server, and you don’t need to learn a complex authentication flow, because the flow is simple:

  1. user clicks on the signin button
  2. a pop-up appears
  3. if not authenticated with Persona, the user is prompted for registration
  4. if authenticated with Persona, the user is prompted for approval of the authentication to your site
  5. the popup closes and the page redirects/refreshes – the user is now signed in

Of course, it is not that simple, but there are just a few things to mind that are not mentioned in the tutorial. So let’s follow the official tutorial step by step, and I’ll extend on each point (the server-side language used is Java, but it’s simple and you can do it in any language)

1. Including the .js file – simple. It is advisable to get the js file from the Mozilla server, rather than store it locally, because it is likely to change (in order to fix bugs, for example). It might be trickier if you merge your js files into one (for the sake of faster page loading), but probably your mechanism allows for loading remote js files.

2. The signin and signout buttons. This looks easy as well. Probably it’s a good idea to add the logout handler conditionally – only if the user has logged in with Persona (as opposed to other authentication methods that your site supports)

3. Listening to authentication events. Listening to events is suggested to be put on all pages (e.g. included in a header template). But there’s a problem here. If your user is already authenticated in Persona, but his session has expired on your site, the script will automatically login the user. And this would require a page reload in a couple of seconds after the page loads. And that’s not necessarily what you or the users want – in my case, for example, this may mean that the track they have just played is interrupted because of page refresh. It can be done with AJAX of course, but it is certainly confusing when something in the UI changes for no apparent reason. Below I’ll show a fix for that. Also, the logout listener might not be needed everywhere – as far as I understand it will automatically logout the user in case you have logged out of Persona. This might not be what you want – for example users might want to keep open tabs with some documents that are not accessible when logged out.

4. Verifying the assertion on the server. Here you might need a 3rd party library in order to invoke the verification endpoint and the parse the json result, but these are pretty standard libraries that you probably already have included.

Now, how to solve the problem with automatic authentication? Declare a new variable – userRequestedAuthentication – that holds whether the authentication has been initiated explicitly by the user, or it has been automatic. In the signin button click handler set that variable to true. Here’s how the js code looks like (btw, I think it’s ok to put the code in document.ready(), rather than directly within the script tag. Assuming you later need some DOM resources in the handler methods, it would be good to have the page fully loaded. On the other hand, this may slow down the process a bit). Note that you can include an empty onlogin handler on all pages, and have the complete one only on the authentication page. But given that the login buttons are either on the homepage, or shown with a javascript modal window, it’s probably ok having it everywhere/on multiple pages.

<script type='text/javascript'>
    var loggedInUser = ${context.user != null ? ''' + context.user.email + ''' : 'null'};
    var userRequestedAuthentication = false;
    navigator.id.watch({
        loggedInUser : loggedInUser,
        onlogin : function(assertion) {
            $.ajax({
                type : 'POST',
                url : '${root}/persona/auth',
                data : {assertion : assertion, userRequestedAuthentication : userRequestedAuthentication},
                success : function(data) {
                    if (data != '') {
                        window.location.href = '${root}' + data;
                    }
                },
                error : function(xhr, status, err) {
                    alert('Authentication failure: ' + err);
                }
            });
        },
        onlogout : function() {
            window.locaiton.open('${root}/logout');
        }
    });
</script>

As you can see, the parameter is passed to the server-side code. What happens there?

@RequestMapping('/persona/auth')
@ResponseBody
public String authenticateWithPersona(@RequestParam String assertion,
        @RequestParam boolean userRequestedAuthentication, HttpServletRequest request, Model model)
        throws IOException {
    if (context.getUser() != null) {
        return '';
    }
    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add('assertion', assertion);
    params.add('audience', request.getScheme() + '://' + request.getServerName() + ':' + (request.getServerPort() == 80 ? '' : request.getServerPort()));
    PersonaVerificationResponse response = restTemplate.postForObject('https://verifier.login.persona.org/verify', params, PersonaVerificationResponse.class);
    if (response.getStatus().equals('okay')) {
        User user = userService.getUserByEmail(response.getEmail());
        if (user == null && userRequestedAuthentication) {
            return '/signup?email=' + response.getEmail();
        } else if (user != null){
            if (userRequestedAuthentication || user.isLoginAutomatically()) {
                context.setUser(user);
                return '/';
            } else {
                return '';
            }
        } else {
            return ''; //in case this is not a user-requested operation, do nothing
        }
    } else {
        logger.warn('Persona authentication failed due to reason: ' + response.getReason());
        throw new IllegalStateException('Authentication failed');
    }
}

The logic looks more convoluted than you’d like it to be, but let me explain:

  • As you can see in the javascript code, an empty string means “do nothing”. If anything else is returned, the javascript opens that page. If not using spring-mvc, instead of returning a @ResponseBody String from the method, you would write that to the response output stream (or in php terms – echo it)
  • First you check if there’s an already authenticated user in the system. If there is, do nothing. I’m not sure if there’s a scenario when Persona invokes “onlogin” on an already authenticated user, but if you are using other authentication options, Persona won’t know that your user has logged in with, say, twitter.
  • Then you invoke the verification url and parse the result to JSON. I’ve used RestTemplate, but anything can be used – HttpClient, URLConnection. For the JSON parsing spring uses Jackson behind the scene. You just need to write a value-object that holds all the properties that Persona might return. So far I’ve only included: status, email and reason (jackson detail: ignoreUnknown=true, spring-mvc detail: you need FormHttpMessageConverter set to the RestTemplate). It is important that the “audience” parameter is exactly the domain the user is currently on. It makes a difference if it’s with www or not, so reconstruct that rather than hardcoding it or loading it from properties. Even if you redirect from www to no-www (or vice-versa), you should still dynamically obtain the url for the sake of testing – your test environments don’t have the same url as the production one.
  • If Persona authentication is “okay”, then you try to locate a user with that email in your database.
  • If there is no such user, and the authentication action has been triggered manually, then send the user to a signup page and supply the email as parameter (you can also set it in the http session, so that the user can’t modify it). The registration page then asks for other details – name, username, date of birth, or whatever you see fit (but keep that to minimum – ideally just the full name). If you only need the email address and nothing else, you can skip the registration page and force-register the user. After the registration is done, you login the user. Note that in case you have stored the email in session (i.e. the user cannot modify it from the registration page), you can skip the confirmation email – the email is already confirmed by Persona
  • If there is a user with that email in your database, check if the action has been requested by the user or whether he has indicated (via a checkbox in the registration page) that he wants to be automatically logged in. This is a thing to consider – should the user be asked about that, or it should always be set to either true or false? I’ve added the checkbox. If login should occur, then set the user in the session and redirect to home (or the previous page, or whatever page is your ‘user home’)(“context” is a session-scoped bean. You can replace it with session.setAttribute('user', user)). If the authentication attempt was automatic, but the user doesn’t want that, do nothing. And the final “else” is for the case when the user doesn’t have an account on your site and an automatic authentication has been triggered – do nothing in that case, otherwise you’ll end up with endless redirects to the registration page
  • in case of failed authentication be sure to log the reason – then you can check if everything works properly by looking at the logs

A cool side-effect of using the email as unique identifier (make the database column unique) is that if you add Persona later to your site, users can login with it even though they have registered in a different way – e.g. facebook or regular registration. So they can set their password to something long and impossible to remember and continue logging in only with Persona.

The details I omitted from the implementation are trivial: the signup page simply gathers fields and submits them to a /completeRegistration handler that stores the new user in the database. The /logout url simply clears the session (and clears cookies if you have stored any). By the way, if automatic signin is enabled, and Persona is your only authentication method, you may not need to store cookies for the sake of keeping the user logged in after the session expires.

Overall, the implementation is still simple, even with the points I made. Persona looks great and I’d like to see it on more sites soon.
 

Reference: A Guide To Authenticating Users With Mozilla Persona from our JCG partner Bozhidar Bozhanov at the Bozho’s tech blog blog.

Bozhidar Bozhanov

Senior Java developer, one of the top stackoverflow users, fluent with Java and Java technology stacks - Spring, JPA, JavaEE, as well as Android, Scala and any framework you throw at him. creator of Computoser - an algorithmic music composer. Worked on telecom projects, e-government and large-scale online recruitment and navigation platforms.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button