6

I have a customer that is trying to access their calendars from our web application. Everything works for all of our other customers, so I am not sure what is different here except this customer is in Australia and using a non gmail.com email address.

The customer is able to authorize our application and we do get a oauth token for the user. We request calendar access and the customer granted it. When we request a list of all of the calendars, we get the invalid grant message.

Below is the code that we use to access their calendars. The method being called is GetAllWritableCalendars.

public class GoogleCalendarAdapter : ICalendarAdapter {
    #region attributes
    private readonly ISiteAuthTokenQueryRepository _tokenRepo;
    private readonly GoogleCalendarSettings        _settings;

    private const string APPNAME = "SomeAppName";

    private const string ACL_OWNER = "owner";
    private const string ACL_WRITER = "writer";
    #endregion

    #region ctor
    public GoogleCalendarAdapter(ISiteAuthTokenQueryRepository tokenRepo,
                                 GoogleCalendarSettings        settings) {
        _tokenRepo = tokenRepo;
        _settings  = settings;
    }
    #endregion

    #region methods
    private GoogleAuthorizationCodeFlow BuildAuthorizationCodeFlow() {
        return new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer() {
            ClientSecrets = BuildClientSecrets(),
            Scopes        = BuildScopeList()
        });
    }

    private CalendarService BuildCalendarService(SiteAuthToken token) {

        return new CalendarService(new BaseClientService.Initializer() {
                ApplicationName       = APPNAME,
                HttpClientInitializer = BuildUserCredential(token)
        });
    }

    private ClientSecrets BuildClientSecrets() {
        return new ClientSecrets() {
            ClientId = _settings.ClientId,
            ClientSecret = _settings.ClientSecret
        };
    }

    private string[] BuildScopeList() {
        return new [] { CalendarService.Scope.Calendar };
    }

    private UserCredential BuildUserCredential(SiteAuthToken token) {
        TokenResponse responseToken = new TokenResponse() {
            AccessToken  = token.AccessToken,
            RefreshToken = token.RefreshToken
        };

        return new UserCredential(BuildAuthorizationCodeFlow(), APPNAME, responseToken);
    }

    public async Task<List<Cal>> GetAllWritableCalendars(Guid siteGuid) {
        SiteAuthToken token = await GetToken(siteGuid);
        CalendarService svc = BuildCalendarService(token);

        IList<CalendarListEntry> calendars = svc.CalendarList
                                                .List()
                                                .Execute()
                                                .Items;

        return calendars.Where(c => c.AccessRole.Equals(ACL_OWNER,  StringComparison.CurrentCultureIgnoreCase) ||
                                    c.AccessRole.Equals(ACL_WRITER, StringComparison.CurrentCultureIgnoreCase))
                        .Select(c => new Cal() {
                            Id   = c.Id,
                            Name = c.Summary
                        })
                        .OrderBy(o => o.Name)
                        .ToList();
    }

    private async Task<SiteAuthToken> GetToken(Guid siteGuid) {
        SiteAuthToken retVal = await _tokenRepo.GetSiteAuthToken(siteGuid);

        if (retVal == null) {
            throw new ApplicationException($"Could not find a SiteAuthToken for specified site (SiteGuid: {siteGuid})");
        }

        return retVal;
    }

    #endregion
LPLN
  • 475
  • 1
  • 6
  • 20
fizch
  • 2,599
  • 3
  • 30
  • 45
  • Is this customer on a G Suite domain? Your customer can check if he has granted access to your application [here](https://myaccount.google.com/permissions). – Kessy Dec 11 '19 at 08:36
  • I deleted my setup that was working before and now it is not working. I have effectively recreated their issue. I have tried changing scopes and I am still getting invalid grants messages. – fizch Dec 12 '19 at 18:54
  • Since the same code is working for all the other customers and the only real difference from this customer is that he is using a non gmail account, I recommend you to check on [G Suite](https://support.google.com/a/answer/1047213?hl=en) support to see if their account have something wrong. Come back if they give you a solution. – Kessy Dec 13 '19 at 08:22
  • It affects me now as well. I am using a gmail address. It's like the permissions requested while acquiring the token aren't actually granted. This is only happening for new users. Existing users aren't having this issue. – fizch Dec 13 '19 at 16:46
  • https://blog.timekit.io/google-oauth-invalid-grant-nightmare-and-how-to-fix-it-9f4efaf1da35?gi=3def3d67a87b – LPLN Dec 14 '19 at 06:09
  • You can try to generate new tokens for this application on the Google Cloud Platform admin page. – Kessy Dec 16 '19 at 13:37
  • 1
    @LPLN while that is a good read, that is something that i had seen before. I had already been down that road. Thanks for the refresher, it is a good article. – fizch Dec 16 '19 at 17:16
  • @Kessy if i generate new tokens for my app, is that going to break the connection for my existing users? – fizch Dec 16 '19 at 17:17
  • 1 - In your case, did you delete your tokens file when you removed your set-up? If you didn't you are using invalid tokens to access, so delete the token.json and try again, it will prompt you to allow access again and that is it. 2 - For your customer: Is he using a G Suite account? if he is not, he doesn't have any google data. If he is within a G Suite domain, it could be that the domain has blacklisted your app or has restrictive settings for third party apps. In this situation check with him. 3 -Which type of authentication are you using? API key, OAuth Client ID or Service Account Key? – Kessy Dec 17 '19 at 09:08
  • Let's not worry about my customer. I am fairly confident that I am getting the same error. In response to number 1, I do not have a token.json. I have my tokens saved in a database table. I deleted that record before reauthorizing, so that should satisfy that requirement. When requesting the token, I am passing client_id, so I think I am using the OAuth Client ID. – fizch Dec 17 '19 at 15:26

2 Answers2

3

The credentials are the authorization from Google to Your Application to use the scopes you have set-up, this is okay to have it in a database if you update it every time you add new scopes to your app.

The Access Token is the authorization from the user to your application to get it's Google Data (calendar in this case). It has a limited lifetime so this is not okay to save in a database.

The Refresh Token is the token that allows your application to get more tokens for a client. It has a limited lifetime as well.

For more info see: Using OAuth 2.0 to Access Google APIs

Every time you change your scopes or add more scopes you have to re-generate the credentials. You have 50 refresh tokens per user account per client, see Token expiration. So having the tokens in a database makes no sense since they are going to get deprecated at some point, if you have 51 clients the 1st token will get deprecated.

Check:

  1. How do you have it set-up on your database
  2. If you renew properly the tokens
  3. If you are using the correct tokens for the users

You can delete all the tokens (NOT the CREDENTIALS) and your current users will only have to go through the consent screen and allow it again, they will not lose the connection.

Kessy
  • 1,894
  • 1
  • 8
  • 15
  • I do store the refresh token as well so that when the token expires, i can update the access token. My scope has not changed. I just ask for all access to the calendar because I need read/write access and be able to list all of their calendars. You can see how I am using the credentials in the code sample from the original post. The method that is being called as entrypoint to this logic is GetAllWritableCalendars. – fizch Dec 18 '19 at 21:44
  • As far as I can see with the information you have provided the problem is either one in the link @LPLN provided or what i said, deleting all the tokens. If it's not you will have to provide more information and more code samples to see your updating token process, how do you store It in the database, etc. – Kessy Dec 19 '19 at 09:19
0

I asked the question later in a different way. Maybe it was a little more relevant. Perhaps there was a little more information available. What ever the case may be, I discovered how to test things properly.

Look at this question

fizch
  • 2,599
  • 3
  • 30
  • 45