Google Auth Code Flow requesting token generates invalid client error when exchanging ID token for Access Token

Viewed 417

Fairly new in looking at the Google OAuth library, but have spent a while in Azure/Exchange OAuth. We have a wrapper around the various libraries in the Adapter Pattern, so our app code is consistent and calls the same methods. Here's what the Google one looks like

private string _state;
private GoogleAuthorizationCodeFlow _flow;

public GoogleTokenProvider(IOAuthSettings settings, string state, string baseUrl) : base(settings, state, baseUrl)
{
    //https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web-applications-asp.net-mvc
    _state = state;
    GoogleAuthorizationCodeFlow.Initializer init = new GoogleAuthorizationCodeFlow.Initializer {
        ClientSecrets = new ClientSecrets
        {
            ClientId = settings.ClientId,
            ClientSecret = settings.ClientSecret
        },
        Scopes = this.Scopes,
        DataStore = new GoogleTokenStore(settings) //Our implementation - not called yet
    };
    _flow = new GoogleAuthorizationCodeFlow(init);
}

The first step (once constructed) is to get the url to start the auth process.

public string GetAuthorizationUrl()
{
    //Using Flow 
    var req = _flow.CreateAuthorizationCodeRequest(this.RedirectUrl);
    var baseUrl = req.Build().AbsoluteUri;
    baseUrl += "&state=" + this._state;
    return baseUrl;

    // Manually build request
    //StringBuilder builder = new StringBuilder();
    //builder.Append("https://accounts.google.com/o/oauth2/v2/auth?");
    //builder.AppendFormat("scope={0}", String.Join("+", this.Scopes));
    //builder.Append("&access_type=offline&include_granted_scopes=true&response_type=code");
    //builder.AppendFormat("&state={0}&", this._state);
    //builder.AppendFormat("&redirect_uri={0}", this.RedirectUrl);
    //builder.AppendFormat("&client_id={0}", this.settings.ClientId);
    //return builder.ToString();
}

This works, and we're directed off to the OAuth site, we log in with our Google account. Our callback is fired and when debugging the callback, we obtain the code, and state passed through and call this method:

public string AcquireTokenByAuthorizationCode(string code)
{
    Google.Apis.Auth.OAuth2.Responses.TokenResponse result = _flow.ExchangeCodeForTokenAsync("userid", code, RedirectUrl, new System.Threading.CancellationToken()).Result;
    return result.AccessToken;
}

However, this causes an error: Error details

Error:"invalid_client", Description:"Unauthorized", Uri:""

There are two queries.

  1. I am unsure what "UserId" should be, a lot of posts just have "userid" or "me" in there, so what it is for?
  2. Since the Flow class is built by the same settings, why does the client suddenly become invalid.

Thanks in advance.

Google Configuration

Google Web Client created

The intent is to be able to read the emails (Gmail) from this account

Enabled APIs:

  • Gmail API
  • Google+ API

google Data Store Implementation

public class GoogleTokenStore : IDataStore
{
    private readonly IOAuthSettings _settings;
    public GoogleTokenStore(IOAuthSettings settings)
    {
        this._settings = settings;
    }
    private Dictionary<string, T> Decode<T>()
    {
        using (var memoryStream = new MemoryStream())
        {
            var binaryFormatter = new BinaryFormatter();
            
            memoryStream.Write(_settings.TokenStore, 0, _settings.TokenStore.Length);
            memoryStream.Seek(0, SeekOrigin.Begin);
            return binaryFormatter.Deserialize(memoryStream) as Dictionary<string, T>;
        }
    }

    private void Encode<T>(Dictionary<string, T> store)
    {
        using (var memoryStream = new MemoryStream())
        {
            var binaryFormatter = new BinaryFormatter();
            binaryFormatter.Serialize(memoryStream, store);
            _settings.TokenStore = memoryStream.ToArray();
        }
    }

    public Task ClearAsync()
    {
        _settings.TokenStore = null;
        return Task.CompletedTask;
    }

    public Task DeleteAsync<T>(string key)
    {
        if (_settings.TokenStore == null)
        {
            return Task.CompletedTask; //THIS IS CALLED, RETURNS HERE
        }
        var store = Decode<T>();
        if (store.ContainsKey(key))
        {
            store.Remove(key);
            Encode<T>(store);
        }
        return Task.CompletedTask;
    }

    public Task<T> GetAsync<T>(string key)
    {
        var store = Decode<T>();
        if (store.ContainsKey(key))
        {
            return Task.FromResult( store[key] );
        }
        return null;
    }

    public Task StoreAsync<T>(string key, T value)
    {
        var store = Decode<T>();
        if(store != null)
        {
            store.Add(key, value);
            Encode<T>(store);
        }
        return Task.CompletedTask;
    }
}

Stack trace of the Google error

   at Google.Apis.Auth.OAuth2.Responses.TokenResponse.<FromHttpResponseAsync>d__36.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Google.Apis.Auth.OAuth2.Requests.TokenRequestExtenstions.<ExecuteAsync>d__1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Google.Apis.Auth.OAuth2.Flows.AuthorizationCodeFlow.<FetchTokenAsync>d__35.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Google.Apis.Auth.OAuth2.Flows.AuthorizationCodeFlow.<FetchTokenAsync>d__35.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Google.Apis.Auth.OAuth2.Flows.AuthorizationCodeFlow.<ExchangeCodeForTokenAsync>d__30.MoveNext()

I have included this code as it does appear the Delete was called and on returning Task.CompletedTask the error appears (on initial call the store is null so just return Task.CompletedTask

1 Answers

Invalid_client error normally means that you have created one type of client on Google Developer console and are using the code for a different type of client.

There are two types of clients normally used with .net applications Desktop app, and web browser client. The code used for them is different. You are appear to be using the MCV code. I would double check that you have created a web browser client on Google developer console.

update from comments

If you check the documentation MVC

Check the section for AppFlowMetadata

You are going to have a problem because its looking for a session var on each users browser

var user = controller.Session["user"];

It uses that session var to know which users data to load from

DataStore = new FileDataStore("Drive.Api.Auth.Store")

So if you want to make this single user make sure to always set the session var to that of the user whos data you want to load. check %appData% its where file data store default stores its credential files for each user. Check this if your curios what IDataStore is

Related