Google+ API: How can I use RefreshTokens to avoid requesting access every time my app launches?

16,827

Solution 1

Here is an example. Make sure you add a string setting called RefreshToken and reference System.Security or find another way to safely store the refresh token.

    private static byte[] aditionalEntropy = { 1, 2, 3, 4, 5 };

    private static IAuthorizationState GetAuthorization(NativeApplicationClient arg)
    {
        // Get the auth URL:
        IAuthorizationState state = new AuthorizationState(new[] { PlusService.Scopes.PlusMe.GetStringValue() });
        state.Callback = new Uri(NativeApplicationClient.OutOfBandCallbackUrl);

        string refreshToken = LoadRefreshToken();
        if (!String.IsNullOrWhiteSpace(refreshToken))
        {
            state.RefreshToken = refreshToken;

            if (arg.RefreshToken(state))
                return state;
        }

        Uri authUri = arg.RequestUserAuthorization(state);

        // Request authorization from the user (by opening a browser window):
        Process.Start(authUri.ToString());
        Console.Write("  Authorization Code: ");
        string authCode = Console.ReadLine();
        Console.WriteLine();

        // Retrieve the access token by using the authorization code:
        var result = arg.ProcessUserAuthorization(authCode, state);

        StoreRefreshToken(state);
        return result;
    }

    private static string LoadRefreshToken()
    {
        return Encoding.Unicode.GetString(ProtectedData.Unprotect(Convert.FromBase64String(Properties.Settings.Default.RefreshToken), aditionalEntropy, DataProtectionScope.CurrentUser));
    }

    private static void StoreRefreshToken(IAuthorizationState state)
    {
        Properties.Settings.Default.RefreshToken = Convert.ToBase64String(ProtectedData.Protect(Encoding.Unicode.GetBytes(state.RefreshToken), aditionalEntropy, DataProtectionScope.CurrentUser));
        Properties.Settings.Default.Save();
    }

Solution 2

The general idea is as follows:

  1. You redirect the user to Google's Authorization Endpoint.

  2. You obtain a short-lived Authorization Code.

  3. You immediately exchange the Authorization Code for a long-lived Access Token using Google's Token Endpoint. The Access Token comes with an expiry date and a Refresh Token.

  4. You make requests to Google's API using the Access Token.

You can reuse the Access Token for as many requests as you like until it expires. Then you can use the Refresh Token to request a new Access Token (which comes with a new expiry date and a new Refresh Token).

See also:

Solution 3

I also had problems with getting "offline" authentication to work (i.e. acquiring authentication with a refresh token), and got HTTP-response 400 Bad request with a code similar to the OP's code. However, I got it to work with the line client.ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(this.clientSecret); in the Authenticate-method. This is essential to get a working code -- I think this line forces the clientSecret to be sent as a POST-parameter to the server (instead of as a HTTP Basic Auth-parameter).

This solution assumes that you've already got a client ID, a client secret and a refresh-token. Note that you don't need to enter an access-token in the code. (A short-lived access-code is acquired "under the hood" from the Google server when sending the long-lived refresh-token with the line client.RefreshAuthorization(state);. This access-token is stored as part of the auth-variable, from where it is used to authorize the API-calls "under the hood".)

A code example that works for me with Google API v3 for accessing my Google Calendar:

class SomeClass
{

    private string clientID         = "XXXXXXXXX.apps.googleusercontent.com";
    private string clientSecret     = "MY_CLIENT_SECRET";
    private string refreshToken     = "MY_REFRESH_TOKEN";
    private string primaryCal       = "MY_GMAIL_ADDRESS";

    private void button2_Click_1(object sender, EventArgs e)
    {
        try
        {
            NativeApplicationClient client = new NativeApplicationClient(GoogleAuthenticationServer.Description, this.clientID, this.clientSecret);
            OAuth2Authenticator<NativeApplicationClient> auth = new OAuth2Authenticator<NativeApplicationClient>(client, Authenticate);

            // Authenticated and ready for API calls...

            // EITHER Calendar API calls (tested):
            CalendarService cal = new CalendarService(auth);
            EventsResource.ListRequest listrequest = cal.Events.List(this.primaryCal);
            Google.Apis.Calendar.v3.Data.Events events = listrequest.Fetch();
            // iterate the events and show them here.

            // OR Plus API calls (not tested) - copied from OP's code:
            var plus = new PlusService(auth);
            plus.Key = "BLAH";  // don't know what this line does.
            var me = plus.People.Get("me").Fetch();
            Console.WriteLine(me.DisplayName);

            // OR some other API calls...
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error while communicating with Google servers. Try again(?). The error was:\r\n" + ex.Message + "\r\n\r\nInner exception:\r\n" + ex.InnerException.Message);
        }
    }

    private IAuthorizationState Authenticate(NativeApplicationClient client)
    {
        IAuthorizationState state = new AuthorizationState(new string[] { }) { RefreshToken = this.refreshToken };

        // IMPORTANT - does not work without:
        client.ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(this.clientSecret);

        client.RefreshAuthorization(state);
        return state;
    }
}

Solution 4

The OAuth 2.0 spec is not yet finished, and there is a smattering of spec implementations out there across the various clients and services that cause these errors to appear. Mostly likely you're doing everything right, but the DotNetOpenAuth version you're using implements a different draft of OAuth 2.0 than Google is currently implementing. Neither part is "right", since the spec isn't yet finalized, but it makes compatibility something of a nightmare.

You can check that the DotNetOpenAuth version you're using is the latest (in case that helps, which it might), but ultimately you may need to either sit tight until the specs are finalized and everyone implements them correctly, or read the Google docs yourself (which presumably describe their version of OAuth 2.0) and implement one that specifically targets their draft version.

Solution 5

I would recommend looking at the "SampleHelper" project in the Samples solution of the Google .NET Client API:

This file shows both how to use Windows Protected Data to store a Refresh token, and it also shows how to use a Local Loopback Server and different techniques to capture the Access code instead of having the user enter it manually.

One of the samples in the library which use this method of authorization can be found below:

Share:
16,827

Related videos on Youtube

Danny Tuppeny
Author by

Danny Tuppeny

Updated on August 28, 2020

Comments

  • Danny Tuppeny
    Danny Tuppeny over 3 years

    I'm trying to use the Google+ API to access info for the authenticated user. I've copied some code from one of the samples, which works fine (below), however I'm having trouble making it work in a way I can reuse the token across app-launches.

    I tried capturing the "RefreshToken" property and using provider.RefreshToken() (amongst other things) and always get a 400 Bad Request response.

    Does anyone know how to make this work, or know where I can find some samples? The Google Code site doesn't seem to cover this :-(

    class Program
    {
        private const string Scope = "https://www.googleapis.com/auth/plus.me";
    
        static void Main(string[] args)
        {
            var provider = new NativeApplicationClient(GoogleAuthenticationServer.Description);
            provider.ClientIdentifier = "BLAH";
            provider.ClientSecret = "BLAH";
            var auth = new OAuth2Authenticator<NativeApplicationClient>(provider, GetAuthentication);
    
            var plus = new PlusService(auth);
            plus.Key = "BLAH";
            var me = plus.People.Get("me").Fetch();
            Console.WriteLine(me.DisplayName);
        }
    
        private static IAuthorizationState GetAuthentication(NativeApplicationClient arg)
        {
            // Get the auth URL:
            IAuthorizationState state = new AuthorizationState(new[] { Scope });
            state.Callback = new Uri(NativeApplicationClient.OutOfBandCallbackUrl);
            Uri authUri = arg.RequestUserAuthorization(state);
    
            // Request authorization from the user (by opening a browser window):
            Process.Start(authUri.ToString());
            Console.Write("  Authorization Code: ");
            string authCode = Console.ReadLine();
            Console.WriteLine();
    
            // Retrieve the access token by using the authorization code:
            return arg.ProcessUserAuthorization(authCode, state);
        }
    }
    
    • Chris Sears
      Chris Sears over 12 years
      Are you seeing the error even when the refresh token is used before the expiration date? If so, is it possible that you are trying to use the same refresh token more than once?
    • Danny Tuppeny
      Danny Tuppeny over 12 years
      I don't really know how I'm supposed to use it - I was just randomly grabbing the Refresh token from the request and then using the RefreshToken method. I can't actually find any documentation on how it should work, so it's possible I was using it incorrectly. That's what I'm trying to figure out :-(
    • poplitea
      poplitea over 11 years
      @DannyTuppeny Have you solved this? If not, have you had a look at my answer? I had the same problem as you, and found no good documentation by googling, but I think I solved the problem.
    • Danny Tuppeny
      Danny Tuppeny over 11 years
      @poplitea I saw your answer (which looks good), but haven't had a chance to try it out (or the others here) :(
  • Nick
    Nick over 12 years
  • Danny Tuppeny
    Danny Tuppeny over 11 years
    I can't get this to compile - the version of the API DLL I just downloaded doesn't have a ClientCredentialApplicator on NativeWebApplication (not do I even have the ability to find a Secret Key in the API console anymore?!)
  • poplitea
    poplitea over 11 years
    You must have using DotNetOpenAuth.OAuth2; in your class, as well as having downloaded DotNetOpenAuth from here: dotnetopenauth.net . Secret Key is called Client Secret (If it isn't there, maybe you need to add your client to the API console once more?)
  • Danny Tuppeny
    Danny Tuppeny over 11 years
    I actually got this working using Lars Truijens's answer; it worked perfectly first time :-)
  • poplitea
    poplitea over 11 years
    @DannyTuppeny Perfect :-) But did you have to use the line client.ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(this.clientSecret);‌​?
  • Danny Tuppeny
    Danny Tuppeny over 11 years
    I didn't; I just took the code from the Google Drive Quickstart (developers.google.com/drive/quickstart) then called the RefreshToken stuff that Lars put in his answer; I can put it all in a gist if it's helpful
  • poplitea
    poplitea over 11 years
    @DannyTuppeny Hmm, okay - that line was what solved my problem.. Don't know what makes my code without that line different from Lars' code, except from storing/retrieving the secret as opposed to hardcoding it within the code. Maybe my problem was caused by character encoding? I see that Lars operates with Unicode... Anyways, nice your problem was solved.
  • Danny Tuppeny
    Danny Tuppeny over 11 years
    My code is the same as the Drive sample, but changed to G+ and using the RefreshToken methods in Lars answer (I didn't really copy any code from his). Here's a gist of my full code: gist.github.com/3658795
  • IPValverde
    IPValverde over 11 years
    Thanks, this helped a lot. I have also used the google class StoredStateClient from the example in that link, so I can retrieve the user information with the same "authorization" response. developers.google.com/drive/credentials
  • Gautam Jain
    Gautam Jain over 10 years
    This solution worked for me. I used to get "Error occurred while sending a direct message or getting the response" 404 Bad Request error. Thanks.