2011 January 05

Google Apps Authentication for Internal Company Sites

Recently I was challenged by my company's CEO to make accessing internal sites as easy as accessing our email.

He needed to get to an internal app from the road. He didn't have a computer that was blessed with a VPN configuration, and he's not the type to be excited about setting up SSH tunnels.

We use Google Apps for email, calendaring, document sharing, and documentation. We can access any of these services from any computer in the world, as long as we have our company domain login and password. Google seems to do a very good job of protecting this extremely sensitive information.

When the CEO threw down the access gauntlet, at first I was obstinate. "But this is _really_ sensitive information you're trying to access! It needs to be encrypted. It needs to be logged. Access to an employee needs to be taken away when the employee leaves the company."

OK deep breath ... then a revelation... Our email has all the necessary protections. Google does one of the best jobs of dealing with security issues around login (bot detection, account lockout, password recovery). The only thing our tool was lacking was the encryption, but I know that I can use Apache as a reverse proxy to add that.

Google supports several authentication mechanisms including OpenID and OAuth. Normally I would shy away from OpenID as a solution due to its strange identification scheme (your identity is a URL -- huh?), but as you will see there are some clever workarounds if you are securing a single Google Apps domain.

This whole thing is made super easy due to the hard work of everyone who contributed to libopkele and mod_auth_openid. Thank you!

  • Step 1: Install apache, php, mod_ssl, and some other dependencies ... I use yum on CentOS to do this: sudo yum -y install libtidy-devel curl-devel sqlite-devel pcre-devel httpd-devel mod_ssl php
  • Step 2: Build and install libopkele and mod_auth_openid curl -O http://kin.klever.net/dist/libopkele-2.0.4.tar.gz tar -xzvf libopkele-2.0.4.tar.gz cd libopkele-2.0.4 ./configure --with-apr-config=/usr/bin/apr-1-config make sudo make install

    curl -O http://butterfat.net/releases/mod_auth_openid/mod_auth_openid-0.6.tar.gz tar -xzvf mod_auth_openid-0.6.tar.gz cd mod_auth_openid-0.6 ./configure --with-apr-config=/usr/bin/apr-1-config make sudo make install
  • Step 3: Part of the OpenID flow are two hosted "discovery" files. mod_auth_openid expects to fetch these files from http://yourdomain.com/openid and http://yourdomain.com/xrds. Either put these files on your site's main server or run a virtual host on this server to answer to yourdomain.com requests and use /etc/hosts to point yourdomain.com to (since it is this server that is making the requests).

    http://yourdomain.com/openid <?xml version="1.0" encoding="UTF-8"?> <xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)"> <XRD> <Service priority="0"> <Type>http://specs.openid.net/auth/2.0/signon</Type> <URI>https://www.google.com/a/yourdomain.com/o8/ud?be=o8</URI> </Service> </XRD> </xrds:XRDS>

    http://yourdomain.com/xrds <?xml version="1.0" encoding="UTF-8"?> <xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)"> <XRD> <Service priority="0"> <Type>http://specs.openid.net/auth/2.0/server</Type> <URI>https://www.google.com/a/yourdomain.com/o8/ud?be=o8</URI> </Service> </XRD> </xrds:XRDS>

  • Step 4: Create a file called "openid.php" and serve it from the document root of this Apache server. There are a few examples out there, but here is what I put together to make a very seamless experience for my company's users: <html> <head> <title>Google Auto-Login Page</title> <style> body { font-family: Helvetica,Arial,sans-serif; } </style> </head> <body> <h1>Google Auth Bouncer</h1> <form id="openid_form" action="<?php echo $_REQUEST['modauthopenid_referrer'];?>" method="GET"> <input type="hidden" name="openid_identifier" value="http://yourdomain.com/xrds"/> <input type="hidden" name="openid.ns.ext1" value="http://openid.net/srv/ax/1.0" /> <input type="hidden" name="openid.ext1.mode" value="fetch_request" /> <input type="hidden" name="openid.ext1.type.email" value="http://axschema.org/contact/email" /> <input type="hidden" name="openid.ext1.required" value="email" /> </form> <script> function submit_form() { document.getElementById("openid_form").submit(); } </script> <?php if ($_REQUEST['modauthopenid_error'] != "") { ?> <font style="color: red;">There was an error:</font> <b><?php echo $_REQUEST['modauthopenid_error']; ?></b>. <br /><br /> <input type="button" value="Try Again..." onClick="submit_form();"> <br /><br /> Here are the error definitions: <ul> <li><strong>no_idp_found</strong>: This is returned when the there was no identity provider URL found on the identity page given by the user, or if the page could not be downloaded. The user probably just mistyped her identity URL.</li> <li><strong>invalid_id_url</strong>: This is returned when the identity URL given is not syntactically valid.</li> <li><strong>idp_not_trusted</strong>: This is returned when the identity provider of the user is not trusted. This will only occur if you have at least one of <strong>AuthOpenIDTrusted</strong> or <strong>AuthOpenIDDistrusted</strong> set.</li> <li><strong>invalid_nonce</strong>: This is a security error. It generally means that someone is attempting a replay attack, though more innocuous reasons are possible (such as a user who doesn't have cookies enabled refreshing the page).</li> <li><strong>canceled</strong>: This is returned if a user cancels the authentication process.</li> <li><strong>unspecified</strong>: This error can occur for a number of reasons, such a bad signature of the query parameters returned from a user's identity provider. Most likely, the user should simply be instructed to attempt again.</li> </ul> <?php } else { ?> Automatically sending you to Google... <script> submit_form(); </script> <?php } ?> </body> </html> The page auto-submits the form when it is loaded, so there is actually nothing for the user to do! The trick is to make openid_identifier a hardcoded hidden field that points at a file on your domain.

    The form includes parameters to request the user's email address. Google will pass it back in a query string param. A more sophisticated implementation may make use of that address to pass along to the internal app you're protecting.
  • Step 5: Configure Apache to use OpenID auth for this host. The following snippet is inside of a VirtualHost block on my server. You can use it just like you would use Basic auth. <Location /> AuthType OpenID Require valid-user AuthOpenIDLoginPage /openid.php AuthOpenIDTrusted ^https://www.google.com/a/yourdomain.com/o8/ud$ AuthOpenIDCookiePath / AuthOpenIdCookieLifetime 21600 </Location> <Files openid.php> Allow from all Satisfy any </Files>
  • Step 6: Restart Apache and make a request to the default virtual host. You should briefly see the openid.php page informing you that it's going to send you to Google. You'll be redirected to a Google Apps login/authorization page. After logging in and granting access to your app, you'll be redirected back to the URL you had originally requested.