Search code examples
google-oauthgoogle-openid

Google OpenIDConnect: Why am I not getting an 'openid_id' value along with 'sub'?


I've read all the documentation I can find on migrating from Google OpenID 2 to OAuth 2/OpenIDConnect, and am currently using a nice class from phpclasses.org . This class seems to work quite well with both Google and Facebook (haven't yet tried other providers), but I'm having a problem with just one aspect of Google's migration path that is quite critical to me: obtaining the google user's old OpenID identifier in addition to the new OpenIDConnect 'sub' value for that user. I've got users registered in my database only through their old OpenID identifiers.

According to Step 3 in Google's Migration Guide it looks like all I should need to do is add a parameter "openid.realm=http://www.example.com" to the authentication request sent to https://accounts.google.com/o/oauth2/auth.

I looked up in my old code what the realm was that I used for its OpenID registration process (it was 'http://' . $_SERVER['HTTP_HOST'];), and then I made sure that the redirect urls in my application were compatible with that realm.

I added that value (url-encoded) as the value of an openid.realm parameter passed on the authentication request made within the class. But when the class exchanged the token for an access token, it got back the correct email, name, sub, etc, but there was no openid_id parameter present. BTW, my scope parameter is 'openid email profile'

Does anyone have a suggestion for what else I should try, or what I can do to determine what the problem is? Does anyone have successful experience getting the openid_id parameter value in php code? I'd really rather not go the client-side route with their "Sign-in with Google" button, and according to the docs that really shouldn't be necessary (plus there's no particular reason to believe it would solve my problem if I did it).


Solution

  • Just discovered it's in the id_token returned along with the access_token when you exchange the authorization_code for the access_token.

    In the Migration Document, Step 3 first two paragraphs:

    When you send an OpenID Connect authentication request URI to Google as described in Step 1, you include an openid.realm parameter. The response that is sent to your redirect_uri includes an authorization code that your application can use to retrieve an access token and an ID token. (You can also retrieve an ID token directly from the OpenID Connect authentication request by adding id_token to the response_type parameter, potentially saving a back-end call to the token endpoint.)

    The response from that token request includes the usual fields (access_token, etc.), plus an openid_id field and the standard OpenID Connect sub field. The fields you need in this context are openid_id and sub:

    This is confusing and misleading/wrong. What token request? The authentication request returns an authorization code that you can exchange for an access_token and an id_token. The parenthetical remark about adding id_token to the response_type doesn't help much, as the various ways I tried to do that resulted in an error. But in any event, the

    "usual fields (access_token, etc.), plus an openid_id field..."

    is wrong. The access_token never appears in the same list at the openid_id field. The access_token appears in a list with the id_token, and the openid_id field is encoded within the id_token!

    For testing purposes, you can decode an id_token using https://www.googleapis.com/oauth2/v1/tokeninfo?id_token=<string>

    In this documentation I couldn't find a useful description for how to decode an id_token, only caveats about their being sensitive, and how to validate them (though validation is not needed if obtained directly from a google endpoint as is the case here). I downloaded google's php client, and extracted code from it (src/Google/Auth/OAuth2.php and src/Google/Utils.php). And from that it's easy enough to figure out how to decode the id_token string: explode on ., base64_decode element 1, and json_decode that.

    Update 2015-05-21: In reply to @Arthur's "answer", which would have been more appropriate as a comment on this answer. I would have commented on that answer myself, but comments aren't allowed to be very long and don't allow image uploads, plus I thought this extra info improves my answer...

    Below is a screenshot from netbeans/xdebug, showing the array elements I get when decoding the id_token I get. Interesting that the intersection of the fields listed here with the fields listed by @Arthur is the null set. So I suspect that whatever @Arthur is decoding, it is not an id_token of the kind described here. I'm not familiar enough with this stuff even to guess what it is that's being decoded in that answer.

    The elements I see in a decoded id_token

    I'm afraid I don't have the time to dig through the library I use to extract the exact code path that produces the id_token I decoded to get this array using the simple algorithm I described. But I can tell you that the library I use is this: http://www.phpclasses.org/package/7700-PHP-Authorize-and-access-APIs-using-OAuth.html

    Using it just as documented does not give you the id_token you need for this for two reasons:

    1. The pre-configured server for Google with Oauth 2 doesn't handle the openid.realm parameter. To handle that, I added the following server definition to the oauth_configuration.json file:

      "Google-OpenIdConnect":
      {
              "oauth_version": "2.0",
              "dialog_url": "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}&openid.realm={REALM}",
              "offline_dialog_url": "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}&access_type=offline&approval_prompt=force",
              "access_token_url": "https://accounts.google.com/o/oauth2/token"
      },
      
    2. Just after the call to Initialize(), you need to add

      $client->store_access_token_response = true;
      

      Without that, the actual access_token response is not accessible (at least not the way I'm using the class). With those two changes in place, my exact code to get the openid_id using this class is as follows:

      protected function jwt_decode($jwt) {
          $segments = explode(".", $jwt);
          if (count($segments) != 3) {
              throw new Exception("Wrong number of segments in token: $jwt");
          }
          // Parse envelope.
          $envelope = json_decode($this->urlSafeB64Decode($segments[0]), true);
          if (!$envelope) {
              throw new Exception("Can't parse token envelope: " . $segments[0]);
          }
          // Parse token
          $json_body = $this->urlSafeB64Decode($segments[1]);
          $payload = json_decode($json_body, true);
          return $payload;
      }
      
      protected function getOpenid_id() {
          require_once 'Phpclasses/Http/Class.php';
          require_once 'Phpclasses/OauthClient/Class.php';
          require 'Phpclasses/Google/private/keys.php';
          $client = new oauth_client_class;
          $client->configuration_file = $phpclasses_oauth_dir . '/oauth_configuration.json';
          $client->server = 'Google-OpenIdConnect';
          $client->redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . strtok($_SERVER['REQUEST_URI'], '?');
          $client->client_id = $GOOGLE_APPID;
          $client->client_secret = $GOOGLE_APPSECRET;
      
          $client->scope = 'openid email';
          $client->realm = $this->getRequest()->getScheme() . '://' . $this->getRequest()->getHttpHost();
          $me = null;
          if (($success = $client->Initialize())) {
              // set *after* the call to Initialize
              $client->store_access_token_response = true;
              if (($success = $client->Process())) {
                  if (strlen($client->authorization_error)) {
                      $client->error = $client->authorization_error;
                      $success = false;
                  }
                  elseif (strlen($client->access_token)) {
                      $success = $client->CallAPI('https://www.googleapis.com/oauth2/v1/userinfo', 'GET', array(), array('FailOnAccessError' => true), $user);
                      $me = (array) $user;
                      if (!array_key_exists('id_token', $client->access_token_response)) {
                          throw new Exception('No id_token in \$client->access_token_response');
                      }
                      $openid_id = $this->jwt_decode($client->access_token_response['id_token']);
                      $me['openid_id'] = $openid_id;
                  }
              }
              $success = $client->Finalize($success);
          }
          if ($client->exit)
              exit;
          $client->ResetAccessToken();
          if ($success) {
              return $me;
          }
          // Code to handle failure...
      }