Protocol Documentation of OpenID IDP for Google hosted domains

Introduction

OpenID discovery for hosted domains presents some challenges, which are not addressed by the current version (2.0) of OpenID:

    1. Many of of Google-Apps-For-Your-Domain customers don't want to go through the hassle of hosting discovery documents on their site. We therefore need to offer a solution that allows a hosted domain to become an OpenID provider without them having to do much work - preferably, hosting one simple file on their site should be enough to indicate that they are an OpenID provider, while outsourcing the rest of the work to their hosting service.

  1. Normally a domain that is an OpenID identity provider has to make its own website extremely secure from attacks that could modify web pages on the site, but that is hard for small businesses to do. To avoid that requirement, we add additional digital signatures on the OpenID discovery documents that then need to be checked by the relying parties.

We are currently participating in several standardization organizations, such as OASIS and the OpenID Foundation, to generate a next-generation OpenID discovery protocol. In particular, XRD will very likely become the new standard for digitally-signed discovery documents, and LRDD is an evolving discovery standard that enables outsourcing of discovery through a simple file (host-meta). If you want to get involved in those discussions, you could start by reviewing these three threads [1, 2, 3].

While these standards are being refined, we are providing a proof-of-concept implementation of a next-generation OpenID discovery protocol. While some of the details of this proof-of-concept-implementation are different from what the eventual standards are likely to look like (e.g., we're using XRDS instead of XRD for discovery documents, and are using temporary namespaces), we believe all the necessary pieces are there.

We are committed to supporting the standards for next-generation OpenID discovery once they are finalized, but are providing this interim solution for two reasons:

    1. Finalizing and agreeing on standards takes time, but we wanted to give our Google-Apps-For-Your-Domain customers the opportunity to become OpenID providers now.

    2. Providing a proof-of-concept implementation of a discovery protocol can help drive the standardization process forward.

We have implemented the Identity Provider pieces of this discovery protocol at Google, and are providing an open-source implementation of the Relying Party pieces at step2.googlecode.com.

Below you can find a description of the protocol.

IdP Discovery

First, we'll go through an example to perform discovery on example.com (i.e., given the IdP identifier "example.com", find out the OpenID endpoint). IdP Discovery is performed when the RP doesn't already know the user's OpenID, but knows their Identity Provider (in this case, example.com).

Here is the general idea: The host-meta document points to an XRDS that describes host-wide meta-data (which includes the location of the OpenID OP endpoint). The reason the host-meta is not directly pointing to the OP endpoint is that we want that statement ("this is my OP endpoint") to be signed, and host-meta doesn't have signatures. So we're using host-meta simply as a hint as to where to find the signed XRDS document that describes the meta-data for the host.

The host-meta for example.com is usually located at http://example.com/.well-known/host-meta and looks something like this:

Link: <https://www.google.com/accounts/o8/site-xrds?hd=example.com>; rel="describedby http://reltype.google.com/openid/xrd-op"; type="application/xrds+xml"

The RP then would fetch the site's XRDS document (in this case from https://www.google.com/accounts/o8/site-xrds?hd=example.com), which looks like this:

<?xml version="1.0" encoding="UTF-8"?>

<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">

<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">

<ds:SignedInfo>

<ds:CanonicalizationMethod Algorithm="http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets" />

<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />

</ds:SignedInfo>

<ds:KeyInfo>

<ds:X509Data>

<ds:X509Certificate>

MIICgjCCAeugAwIBAgIEAJSCoDANBg....==

</ds:X509Certificate>

<ds:X509Certificate>

MIICsDCCAhmgAwIBAgIDBp...=

</ds:X509Certificate>

</ds:X509Data>

</ds:KeyInfo>

</ds:Signature>

<XRD>

<CanonicalID>example.com</CanonicalID>

<Service priority="0">

<Type>http://specs.openid.net/auth/2.0/server</Type>

<Type>http://openid.net/srv/ax/1.0</Type>

<URI>https://www.google.com/a/example.com/o8/ud?be=o8</URI>

</Service>

<Service priority="0" xmlns:openid="http://namespace.google.com/openid/xmlns">

<Type>http://www.iana.org/assignments/relation/describedby</Type>

<MediaType>application/xrds+xml</MediaType>

<openid:URITemplate>https://www.google.com/accounts/o8/user-xrds?uri={%uri}</openid:URITemplate>

<openid:NextAuthority>hosted-id.google.com</openid:NextAuthority>

</Service>

</XRD>

</xrds:XRDS>

There is a signature in the HTTP response headers, like this:

HTTP/1.1 200 OK

Expires: Fri, 22 May 2009 12:50:18 GMT

...

Signature: gB89TNxvjMbJjLBRjY+ubMHaZ/CODK/l77VGdSTZdgei5k1PxAa/MJ8cP12n5bHwcCVpTNwZjnP3XnjiKNkIDESSzuT7ZyQ2a/tVpge3FgM4R+BgbKfjjayUckVNyC0OYOEeBgq+T27CsBuSEwRpC56A3pcHXC3hfNqxk3zndK0=

The RP should check that

(1) the canonicalID equals the entitity that it is performing discovery on (in this case, "example.com")

(2) the document is signed by an entitiy authoritative for that canonicalId (i.e., the signing cert should be issued to example.com or perhaps some trusted third party)

(3) the certificate chain is anchored in a CA that the RP trusts.

If all this works out, then the RP can send the user to the URI specified in the XRDS (https://www.google.com/a/example.com/o8/ud?be=o8) for login.

User Discovery

Below, we'll go through an example to perform discovery on http://example.com/openid?id=108441225163454056756.

User Discovery happens when the RP already knows the OpenID of the user (as opposed to knowing only the user's IdP), or - more importantly - when the RP receives an auth response from an IdP and needs to verify the authenticity of the auth response. This includes verification of the signature of the auth response, and a discovery operation on the asserted OpenID ("claimed ID"), which should advertise the same OP endpoint as the one included in the auth response.

There are again a variety of ways user discovery would work, but we're implementing one that again starts with a host-meta file. That host-meta file points (as before) to a site-wide XRDS document, which as a UriTemplate in it that allows the RP to construct the URI at which the user's XRDS document resides. The user's XRDS document then mentions the OpenID endpoint.

Here are the detailed steps:

Let's say the OpenID (claimed id) asserted was http://example.com/openid?id=108441225163454056756

The RP extracts from that OpenID URI the host component, and fetches the host-meta for that host (in this example, "example.com"). The host-meta for example.com is usually located at http://example.com/.well-known/host-meta and looks something like this:

Link: <https://www.google.com/accounts/o8/site-xrds?hd=example.com>; rel="describedby http://reltype.google.com/openid/xrd-op"; type="application/xrds+xml"

The RP then would fetch the site's XRDS document, which would look like this:

<?xml version="1.0" encoding="UTF-8"?>

<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">

<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">

<ds:SignedInfo>

<ds:CanonicalizationMethod Algorithm="http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets" />

<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />

</ds:SignedInfo>

<ds:KeyInfo>

<ds:X509Data>

<ds:X509Certificate>

MIICgjCCAeu...FA==

</ds:X509Certificate>

<ds:X509Certificate>

MIICsDCCAhmgAwIBA...LldyLU=

</ds:X509Certificate>

</ds:X509Data>

</ds:KeyInfo>

</ds:Signature>

<XRD>

<CanonicalID>example.com</CanonicalID>

<Service priority="0">

<Type>http://specs.openid.net/auth/2.0/server</Type>

<Type>http://openid.net/srv/ax/1.0</Type>

<URI>https://www.google.com/a/example.com/o8/ud?be=o8</URI>

</Service>

<Service priority="0" xmlns:openid="http://namespace.google.com/openid/xmlns">

<Type>http://www.iana.org/assignments/relation/describedby</Type>

<MediaType>application/xrds+xml</MediaType>

<openid:URITemplate>https://www.google.com/accounts/o8/user-xrds?uri={%uri}</openid:URITemplate>

<openid:NextAuthority>hosted-id.google.com</openid:NextAuthority>

</Service>

</XRD>

</xrds:XRDS>

There is a signature in the HTTP response header, like this:

HTTP/1.1 200 OK

Expires: Fri, 22 May 2009 12:50:18 GMT

...

Signature: gB89TNxvjMbJjLBRjY+ubMHaZ/CODK/l77VGdSTZdgei5k1PxAa/MJ8cP12n5bHwcCVpTNwZjnP3XnjiKNkIDESSzuT7ZyQ2a/tVpge3FgM4R+BgbKfjjayUckVNyC0OYOEeBgq+T27CsBuSEwRpC56A3pcHXC3hfNqxk3zndK0=

The RP does the following:

- it checks the signature on the document as described under "IdP Discovery" above (using the host as the canonical ID, instead of the OpenID)

- it looks for a Service element of Type "http://www.iana.org/assignments/relation/describedby" and extracts the URI template from it

- it applies the OpenID (http://example.com/openid?id=108441225163454056756) to the template to get the URI of the user's XRDS (in this example, it would be https://www.google.com/accounts/o8/user-xrds?uri=http%3a%2f%2fexample.com%2fopenid%3fid%3d108441225163454056756)

- it makes note of the "next authority" that it would expect to have signed the document at that location

- it fetches the document at that location. This will look like this:

<?xml version="1.0" encoding="UTF-8"?>

<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">

<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">

<ds:SignedInfo>

<ds:CanonicalizationMethod Algorithm="http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets" />

<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />

</ds:SignedInfo>

<ds:KeyInfo>

<ds:X509Data>

<ds:X509Certificate>

MIICgjCC...KdA2EFA==

</ds:X509Certificate>

<ds:X509Certificate>

MIICsDCCAhmgA...dyLU=

</ds:X509Certificate>

</ds:X509Data>

</ds:KeyInfo>

</ds:Signature>

<XRD>

<CanonicalID>http://example.com/openid?id=108441225163454056756</CanonicalID>

<Service priority="0">

<Type>http://specs.openid.net/auth/2.0/signon</Type>

<Type>http://openid.net/srv/ax/1.0</Type>

<URI>https://www.google.com/a/example.com/o8/ud?be=o8</URI>

</Service>

</XRD>

</xrds:XRDS>

(Again, the signature on the document is in the HTTP response header)

The RP should check that

- the canonicalID equals the entitity that it is performing discovery on,

- the document is signed by the entitiy delegated to in the URITemplate (which was hosted-id.google.com). (Alternatively - if that template didn't mention any delegation - by an entitiy authoritive for the OpenID, which might mean in this case a certificate issued to example.com)

- the certificate chain is anchored in a CA that the RP trusts.

If that all checks out, the RP extracts the OP endpoint from the XRDS and compares it to the OP endpoint mentioned in the auth response.

Outsourcing of Signing and host-meta Hosting

Many Google Apps domains don't have the means to sign the site's XRDS document, or even to host the host-meta. Google therefore provides a host-meta for such domains, meaning that the host-meta for example.com might be found in two different places:

(1) http://example.com/.well-known/host-meta

(2) https://www.google.com/accounts/o8/.well-known/host-meta?hd=example.com

RPs who follow the standard discovery flow would just use (1). However an RP can choose to also use (2), for example if they have a login box that explicitly asks users to enter a Google Apps domain name.

Google's endpoint will return a 400 on endpoint (2) if the domain provided is not hosted by Google. So one possible strategy for an RP that wants to cater to Google Apps customers could be to first try endpoint (2), and if that returns a 400, try endpoint (1), and if that doesn't yield anything, give up. Other strategies (fetching both endpoints in parallel, for example), are possible.

The host-meta obtained from endpoint (2) will look like this:

Link: <https://www.google.com/accounts/o8/site-xrds?ns=2&hd=example.com>; rel="describedby http://reltype.google.com/openid/xrd-op"; type="application/xrds+xml"

The site's XRDS hosted at https://www.google.com/accounts/o8/site-xrds?ns=2&hd=example.com will be signed by a certificate issued to hosted-id.google.com, regardless of the domain for which the XRDS applies.

Code

The code for relying parties is up on step2.googlecode.com. There is a demo running on www.puffypoodles.com, where you can type the name of a hosted domain into the OpenID field to start the discovery flow.