Thursday, November 07, 2013

openID for JAAS

I finally have my little ToDo app wired up with openID authentication. I'm running an openid4java based relying party, littleId, on heroku that provides the client with a signed web token that the client can present as credentials to other services (yet to be developed!) that share the signing secret.

LittleId includes a JAAS (java authentication and authorization service) login module (littleware.apps.littleId.client.controller.JaasLoginModule) that a web service can plug into its authentication stack to verify littleId web tokens. A service must share the secret littleId signed the token with to verify the token. Currently littleToDo uses the token to authenticate with a do-nothing littleware service that uses this JAAS login configuration (login.config):

> cat ..\littleApps\fishRunner\login.config
littleware.login {
    /*... Stacked LDAP login module ...
        com.sun.security.auth.module.LdapLoginModule SUFFICIENT
        userProvider="ldap://XXXXX/dc=XXXXXX,dc=XXX"
        authIdentity="{USERNAME}"
        userFilter="(cn:dn:={USERNAME})"
        useSSL=false
        debug=true;
        */
        com.sun.jmx.remote.security.FileLoginModule SUFFICIENT
        passwordFile="passwords.properties";

        littleware.apps.littleId.client.controller.JaasLoginModule SUFFICIENT;
};

The openID flow is handled in the browser by a few javascript and typescript modules (on github), and a popup, openIdPop.html. The whole thing relies on CORS (cross origin resource sharing) with XDR cookies enabled (littleId is able to stay stateless on the server side by stashing some data in a cookie) to allow the popup (hosted on the same apps.frickjack.com domain as little-ToDo's assets) to access littleId's REST service running on heroku under littleware.herokuapp.com.

Anyway, the tricky stuff is all handled in the popup. The openIdPop.html popup is actually loaded twice during the openId flow. First, when the parent browser window opens the popup, the user selects the openId provider to authenticate with (currently Google or Yahoo). The selection-handler retrieves the parameters to post to the openID provider (via an XHR request to the littleId service), and submits the data to the provider (Yahoo or Google). After the user authenticates with the openID provider, then the provider redirects the browser back to the littleId service (openID relying party). LittleID verifies that the openID authentication succeeded, and generates a web token that it delivers back to openIdPop.html in the query parameters attached to the browser redirect URL. The code in openIdPop.html looks like this:

    littleware.littleYUI.bootstrap( {/*  classNamePrefix: 'pure'  */ } ).use( 
        'transition', 'littleware-auth-littleId', 'littleware-littleUtil',
        function (Y) {
            var util = Y.littleware.littleUtil;
            var log = new util.Logger("openIdPop.html");
            var littleId = Y.littleware.auth.littleId;

            littleId.helperFactory.get().then(
                function (loginHelper) {

                    // handle provider-selection event
                    Y.one("div#app").delegate('click',
                            function (e) {
                                e.preventDefault();
                                var providerName = e.currentTarget.getAttribute('data-provider');
                                log.log("Authenticating with provider: " + providerName);
                                loginHelper.prepareAuthRequest(providerName).then(
                                    function (request) {
                                        log.log("Check 1");
                                        var formBlock = loginHelper.buildProviderForm(request);
                                        Y.one("body").appendChild(formBlock);
                                        //log.log("Added form to body: " + formBlock.getHTML());
                                        formBlock.one( "form" ).getDOMNode().submit();  // exit to openId provider!
                                    },
                                    function (err) {
                                        alert("Error collecting auth data");
                                        log.log("Auth prep error");
                                        console.dir(err);
                                    }
                                );
                            }, "a.little-idprovider"
                     );

                    // handle auth data present in the URL paremeter if any ...
                    if ( littleId.CallbackData.isCallbackURL( window.location.href ) ) {
                        var callbackData = littleId.CallbackData.buildFromURL(window.location.href);
                        var message = "";
                        if (callbackData.authSuccess) {
                            message = "Authenticated as " + callbackData.userCreds.email +
                                ", secret: " + callbackData.userCreds.secret;
                        } else {
                            message = "Not authenticated";
                        }

                        // little bit of feedback
                        Y.one("div#app").setHTML("<p>" + message + "</p>");
                        
                        // if running as a popup - than call out to parent window, and close this popup
                        if (window.opener) {
                            log.log("Notifying parent window of openId callback ..." );
                            window.opener.littleware.auth.littleId.providerCallback(callbackData).then(
                                function () {
                                    log.log("Parent window callback ok");
                                    window.close();
                                },
                                function (err) {
                                    alert("Failed parent window callback");
                                    log.log("Failed parent window callback");
                                    console.dir(err);
                                }
                              );
                        }
                    }
                }
            );
        }
    );

Fortunately - the host application (little-ToDo) doesn't need to worry about that. The app just launches the popup to initiate an authentication workflow (
window.open("/littleware_apps/auth/openIdPop.html", "openid_popup");
), and listens for the littleId credentials attribute to change:

  littleIdHelper.after("userCredsChange",
      (ev) => {
          var creds = littleIdHelper.get("userCreds");
          ...
       }
  );

Hopefully some of that makes sense. It doesn't look like much, but I'm pretty happy, because this little authentication service ties together a bunch of things I've been working on into one almost coherent mess ( running services on heroku, hosting content on S3, openID, YUI modules with typescript, YUI promises and handlebars templates, responsive mobile-webapp UX ), and hopefully gives me a good pattern to follow for building some other services.

No comments: