PHP
Article
By Vito Tardia

Authenticate Users with Mozilla Persona

By Vito Tardia
Last chance to win! You'll get a... FREE 6-Month Subscription to SitePoint Premium Plus you'll go in the draw to WIN a new Macbook SitePoint 2017 Survey Yes, let's Do this It only takes 5 min

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.

--ADVERTISEMENT--

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

More:
Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the most important and interesting stories in tech. Straight to your inbox, daily.
Is it good?Is it good?