Authenticate Users with Mozilla Persona

Tweet

In this article we’ll see how to use the new, future-ready distributed authentication system Mozilla Persona. As the name says, Persona is created and sponsored by the Mozilla Foundation and presents itself as an easier and more secure alternative to OpenID and OAuth.

The Theory Behind Mozilla Persona

Mozilla Persona is built upon the BrowserID technology also developed by Mozilla. You can find an expanded explanation of the concepts in the article How BrowserID Works by Lloyd Hilaiel, but I will condense the key points here.

  • An email address is an identity and it’s verified by the email provider (such as Gmail, Yahoo, etc.) who serves the role of Primary Identity Authority (or simply primary).
  • Authentication takes place in the browser. There is no transaction with third-party sites so it’s efficient and privacy-protecting. The browser has the role of Implementation Provider (IP).

In the ideal (future) workflow, the browser implements the navigator.id API natively and email providers are able to act as the primary authority for their users. We can implement a functional workflow today by using the HTML5 implementation provided by Mozilla and the browserid.org server as a Secondary Identity Authority to verify email addresses.

Let’s say that Mark wants to sign in to the site myfavoritebeer.org using the email address mr.nice.guy@gmail.com. There are three steps to follow:

1. Certificate provisioning
If it’s the first time the email address is used as an identity, the browser sends Mark to a secure authentication page at GMail. The browser then generates a key pair (using navigator.id.genKeyPair()) and sends the public key to GMail.

GMail sends back a certificate, a signed package containing the email address, the user’s public key and an expiration date. It’s signed with Gmail’s private key and packaged as a JSON Web Token (JWT), a signed JSON Object.

The certificate is stored in the browser keychain along with the private key (using navigator.id.registerVerifiedEmail()) and is used in the next step.

If an email provider does not support BrowserID natively, the email is verified by the browserid.org server as a secondary authority. Basically, you receive an email with a link to visit in order to confirm your identity.

2. Assertion generation
The browser generates an assertion, a JWT package proving that Mark owns mr.nice.guy@gmail.com. The package contains the target site (myfavoritebeer.org), an expiration date, and the certificate issued by GMail. It’s all signed with Mark’s private key and sent to myfavoritebeer.org for verification.

If the browser does not support BrowserID natively then the operation is performed by fallback JavaScript code provided by Mozilla.

3. Assertion verification
The site myfavoritebeer.org receives and verifies Mark’s request. First it checks the validity period, then it retrieves GMail’s public key to verify the certificate. The certificate contains Mark’s public key which is used to verify the assertion.

This step assumes GMail supports BrowserID and shares its public keys. At present, the certificate contains an issued-by property pointing to the secondary authority that generated it.

If it all goes well Mark is logged in to the site until the assertion expires.

Adding Persona Authentication to Your Site

The following example uses and extends the information and code from the official Mozilla Persona Quick Setup article in order to obtain a basic yet fully functional system. The buttons use the CSS styles created for Mozilla by Sawyer Hollenshead. The code for the example can be downloaded from GitHub.

First here’s a basic index.php file:

<?php
// Check if the user is logged in
$user = null;
if (!empty($_COOKIE['auth'])) {
    $user = $_COOKIE['auth'];
}
?>
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>My Favorite Beer - Mozilla Persona Test Code</title>
  <link rel="stylesheet" href="css/persona/persona-buttons.css">
 </head>
 <body>
  <h1>My Favorite Beer: a test page for Mozilla Persona</h1>
<?php
if (!empty($user)) {
?>
  <p>Hello <strong><?=$user?></strong>!</p>
  <a href="#" class="persona-button persona-signout"><span>Sign out</span></a>
<?php
}
else {
?>
  <a href="#" class="persona-button persona-signin"><span>Sign in with Persona</span></a>
<?php
}
?>
  <!--jQuery Library from Google-->
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>

  <!--Persona Library from Mozilla-->
  <script src="https://login.persona.org/include.js"></script>

  <!-- Our custom code -->
  <script src="js/mfb.js"></script>
 </body>
</html>

I’m storing the authentication info (the email address only) in a cookie variable called auth. The first few PHP lines check this cookie and set a local $user variable accordingly.

The $user variable is checked in the main body: if the user is logged in a welcome message is displayed followed by the “Sign out” button. If not the “Sign in” button is displayed by default.

At the bottom of the page you can find the jQuery library, the Mozilla Persona fallback library, and our custom JavaScript code file mfb.js. In this file I’ve set our custom hooks that link the backend to the Persona API.

(function(window, $, undefined) {
    // See: http://www.quirksmode.org/js/cookies.html
    window.readCookie = function(name) {
        var nameEQ = name + '=';
        var ca = document.cookie.split(';');
        for(var i = 0; i < ca.length; i++) {
            var c = ca[i];
            while (c.charAt(0) == ' ') {
                c = c.substring(1, c.length);
            }
            if (c.indexOf(nameEQ) == 0) {
                return c.substring(nameEQ.length, c.length);
            }
        }
        return null;
    };

    // Read auth info (the email address) from cookie
    var currentUser = window.readCookie('auth');

    // If the user is not logged in set the default to null
    if (!currentUser) {
        currentUser = null;
    }

    // The returned value must be URL-decoded
    if (currentUser != null) {
        currentUser = decodeURIComponent(currentUser);
    }

    navigator.id.watch({
        loggedInUser: currentUser,
        onlogin: function(assertion) {
            // A user has logged in! Here you need to send the
            // assertion to your backend for verification and to
            // create a session and then update your UI.
            $.ajax({
                type: 'POST',
                url: 'login.php', // This is a URL on your website.
                data: {assertion: assertion},
                success: function(res, status, xhr) {
                    window.location.reload();
                },
                error: function(xhr, status, err) {
                    alert('Login failure: ' + err);
                }
            });
        },
        onlogout: function() {
            // A user has logged out! Here you need to tear down the
            // user's session by redirecting the user or making a call
            // to your backend. Also, make sure loggedInUser will get
            // set to null on the next page load.
            $.ajax({
                type: 'POST',
                url: 'logout.php', // This is a URL on your website.
                success: function(res, status, xhr) {
                    window.location.reload();
                },
                error: function(xhr, status, err) {
                    alert('Logout failure: ' + err);
                }
            });
        }
    });

    $(document).ready(function(){

        $('a.persona-signin').click(function(e) {
            navigator.id.request();
            e.preventDefault();
        });
        $('a.persona-signout').click(function(e) {
            navigator.id.logout();
            e.preventDefault();
        });
    });
})(window, jQuery);

First there’s a quick utility function that reads cookies. Then it tries to read the currentUser variable from the cookie. It’s URL-encoded so we need the built-in decodeURIComponent() function to obtain the unencoded email address. It’s important that this variable contains either the user’s valid email address or the JavaScript null value; any other value will cause an infinite reload loop.

Next we “watch” the required onlogin and onlogout actions by calling the navigator.id.watch() API and passing the details of our PHP backend. The login.php and logout.php scripts are called via Ajax POST by this API.

The logout script is pretty simple: it unsets the auth cookie and exits. In a real world application it’s good practice to include a CSRF protection token and perform other security checks.

The login script is responsible for validating the user’s identity with the email provider. In a real app we would also check the user’s identity inside the application’s database.

The main flow is:

<?php
// Call the BrowserID API
$response = PersonaVerify();

// If the authentication is successful set the auth cookie
$result = json_decode($response, true);
if ('okay' == $result['status']) {
    $email = $result['email'];
    setcookie('auth', $email);
}

// Print the response to the Ajax script
echo $response;

First it calls a utility function that encapsulates all the business logic (we’ll see it soon). If the identity check is positive then the auth cookie is set and other post-auth code should be called here; in any case the raw JSON response is returned to the calling script.

The function PersonaVerify() is responsible for the assertion and certificate verification. It’s important that this step is performed server-side to limit the possibility of malicious code injection. Normally in this function we should retrieve the public key from the identity authority and verify it ourselves, but since the specifications are still in development and the BrowserID protocol is not implemented by email providers, the safe way to do it is perform remote validation with Persona verification service.

<?php
function PersonaVerify() {
    $url = 'https://verifier.login.persona.org/verify';

    $assert = filter_input(
        INPUT_POST,
        'assertion',
        FILTER_UNSAFE_RAW,
        FILTER_FLAG_STRIP_LOW|FILTER_FLAG_STRIP_HIGH
    );

    $scheme = 'http';
    if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != "on") {
        $scheme = 'https';
    }
    $audience = sprintf(
        '%s://%s:%s',
        $scheme,
        $_SERVER['HTTP_HOST'],
        $_SERVER['SERVER_PORT']
    );

    $params = 'assertion=' . urlencode($assert) . '&audience='
        . urlencode($audience);

    $ch = curl_init();
    $options = array(
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => 2,
        CURLOPT_POSTFIELDS => $params
    );

    curl_setopt_array($ch, $options);
    $result = curl_exec($ch);
    curl_close($ch);
    return $result;
}

The code for this function comes from the Mozilla Persona developers area.

The assertion parameter is taken from the Ajax POSTed variables and sanitized. The audience parameter is essentially our site URL in the format scheme://host:port, so it’s calculated using the $_SERVER superglobal.

Both parameters are encoded and sent using a cURL HTTP POST call to the Persona Verification Service. The result is a JSON string that is returned to the calling script for parsing.

Summary

In a few steps we have easily integrated a new authentication system for our applications. Thanks to the fallback code we can start using it now and be ready when it will be supported natively by both the browser and the email providers.

If you want a deeper knowledge of the topic you can read the Persona Official Documentation and this article that highlights the differences between Persona and OpenID.

That’s all for now, happy coding!

Image via Fotolia

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • eranga

    Nice article. However, if this is only support by FF, I wonder how can I use in real world.

    • http://keryx.se/ Lars Gunther

      There is a JavaScript fallback that makes Persona work in any (JS-enabled) browser.

  • violacase

    Very interesting. Thanks for making time writing this article. I would really appreciate it if you are willing to write follow up articles if new developments arrive!

    • http://www.vtardia.com/ Vito Tardia

      I hope so, I have a personal interest in it :)

  • http://www.eduardocasas.com eduardocasas

    A few weeks ago I developed an example powered with jQuery and PHP that works in every browser with JS enabled. If you want to take a look you can find it in github:
    https://github.com/eduardocasas/Mozilla-Persona-Simple-App-Example

    • http://www.vtardia.com/ Vito Tardia

      I’ve just tried, well done Eduardo!

  • kenneth

    Good article … but CURL is not my favourite … any other way of implementing this without curl?

    • http://www.vtardia.com/ Vito Tardia

      Thanks Kenneth. If you write or find a good wrapper library cURL is the easiest way. In alternative you can use PHP file_get_contents() with the native streams or built a client using sockets. The latter is a good programming exercise but it’s quite complicated.

    • http://zaemis.blogspot.com Timothy Boronczyk

      I believe point number 5 of 5 Inspiring and Useful PHP Snippets is what you’re looking for — Using file_get_contents as a cURL Alternative.

  • http://jpdempsey.net James Dempsey

    Absolutely brilliant walkthrough the new system. I found this resource far more engaging than Mozilla’s explanation, and will definitely come back to this site when further developing my php sites.

    • sudheesh C R

      Hello friends,
      I was trying to execute above mentioned persona integrated sample application. But after login through persona it redirects to same page. Plz anbody can tell me the solution

      • http://www.vtardia.com/ Vito Tardia

        Hi subheesh,

        this example always reload the same page after login or logout. The AJAX success function performs a window.location.reload(). You should replace this statement with your custom url.

        Hope that helps

        Vito

        • Sudheesh

          Thanks a lot Sir. I wanted to have more discussion on regarding this. Sir, I have send you the contact mail on your website.

          • Sudheesh

            Hello Vito,
            As per your suggestion I have changed the window.location=”page1.php” in onlogin method of persona but how to signout in persona. In your example even though I login through persona I cant see Hello and my name.
            Thanks

  • John C

    Thanks for the demo site, which I’ve tried out. This is the 1st demo of Persona I’ve come close to understanding.
    I’m fairly new to PHP but would like a reliable user login system that only allows in memmbers.

    I have a function ValidUser($email), which looks up the email of the user logging in, against the user table in the Database and returned TRUE or FAlse
    How could I fit this into your code?
    eg this fails and I get the infinite loop in the browser.

    $result = json_decode($response, true);
    if (‘okay’ == $result['status']) {
    $email = $result['email'];
    if (ValidateEmail($email)) {
    setcookie(‘auth’, $email);
    echo $response;
    }
    else
    {
    setcookie(‘auth’,””, time() – 3600);
    echo $response;
    }
    }

    • http://www.vtardia.com/ Vito Tardia

      Hi John, I didn’t try it out but I think you get the infinite loop because of your “else” statement which sets an empty string value. Any value other than valid email or Javascript null causes this loop.

  • sudheesh C R

    Hello sir ,
    Thank you for your reply. Sir I have changed window.location=”page1.php” in onlogin method,it works fine. But how can I use persona signout. I tryd ur example, where once I signin through persona and again if i reload the same page I cannot see Hello $user message and signout button.
    waiting for your reply
    Thank you.

    • http://www.vtardia.com/ Vito Tardia

      Hi sudheesh,
      maybe something went wrong, I suggest you to enable error reporting on you php server and check the javascript to see whats going on. I’ve seen you message, if you prefer we can continue the conversation in private mail.


      Vito

      • Sudheesh

        Sure sir , sudheesh088@gmail.com is my official Email ID. May I get your Email ID plz ?, so that we can have a clear discussion on regarding this.
        Thank you

  • http://www.ianmonroe.com Ian Monroe

    I had the infinite loop problem on logout as well (though the rest of your example was quite helpful.)
    I’ve managed to solve it by setting the “auth” cookie to an empty string via the javascript callback. That fixed it right up.
    Hopefully that’ll help anyone else who’s having this problem.