6

I'm trying to use the AccessToken provided by Owin in Google.Apis requests but I'm receiveing the exception System.InvalidOperationException (Additional information: The access token has expired but we can't refresh it).

My configuration of Google Authentication is OK and I can successfully login into my application with it. I store the context.AccessToken as a Claim in the authentication callback (OnAuthenticated "event" of GoogleOAuth2AuthenticationProvider).

My Startup.Auth.cs configuration (app.UseGoogleAuthentication(ConfigureGooglePlus()))

private GoogleOAuth2AuthenticationOptions ConfigureGooglePlus()
{
var goolePlusOptions = new GoogleOAuth2AuthenticationOptions()
{
    ClientId = "Xxxxxxx.apps.googleusercontent.com",
    ClientSecret = "YYYYYYzzzzzz",
    Provider = new GoogleOAuth2AuthenticationProvider()
    {
        OnAuthenticated = context =>
        {
            context.Identity.AddClaim(new System.Security.Claims.Claim("Google_AccessToken", context.AccessToken));
            return Task.FromResult(0);
        }
    },
    SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie
};

goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/plus.login");
goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/userinfo.email");

return goolePlusOptions;

}

The code in which the exception is throwed (Execute() method)

var externalIdentity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);

var accessTokenClaim = externalIdentity.FindAll(loginProvider + "_AccessToken").First();

var secrets = new ClientSecrets()
{
    ClientId = "Xxxxxxx.apps.googleusercontent.com",
    ClientSecret = "YYYYYYzzzzzz"
};

IAuthorizationCodeFlow flow =
    new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
    {
        ClientSecrets = secrets,
        Scopes = new[] { PlusService.Scope.PlusLogin, PlusService.Scope.UserinfoEmail }
    });

UserCredential credential = new UserCredential(flow, "me", new TokenResponse() { AccessToken = accessTokenClaim.Value });

var ps = new PlusService(
    new BaseClientService.Initializer()
    {
        ApplicationName = "My App Name",
        HttpClientInitializer = credential
    });

var k = ps.People.List("me", PeopleResource.ListRequest.CollectionEnum.Visible).Execute();

Is there another way to get the original AccessToken or refresh it without pass thru the entire authentication process (the user is already authenticated)?

I need to query some GooglePlus profile data such as GivenName, familyName, gender, profile picture and profile url.

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449
Douglas Gandini
  • 827
  • 10
  • 24
  • **UPDATE** I got it work using the values obtained via "OAuth 2.0 Playground" and the problem to put it working is that the RefreshToken is null in the context and I don´t know how to get a new one. – Douglas Gandini Jun 25 '15 at 21:46
  • I haven't tried what you are doing before. But when you request access from google you have to also add AccessType offline this will tell the authentication server to return a refreshtoken to you. The client library should handle that for you. – Linda Lawton - DaImTo Jun 26 '15 at 08:16
  • Can you please attach the exception? I'm not sure if you can get the RefreshToken from the externalIdentity, but I would recommend you start investigating it, since the client library has to have a refresh token to keep your access token fresh (it expires after 60 minutes) – peleyal Jun 26 '15 at 10:52
  • @DaImTo I changed the Google authentication options to use the AccessType="offline" and to store aditional informations such as RefreshToken, Id and Expires In. After that it worked! Thanks! – Douglas Gandini Jun 26 '15 at 15:31
  • @peleyal it worked after Linda's hints. Thanks, – Douglas Gandini Jun 26 '15 at 15:31
  • Sweet... If you think that the client library should be improved in order to support this case, open a bug in our issue tracker - https://github.com/google/google-api-dotnet-client/issues and try to add as many details as possible. I'll be happy to hear your suggestion (Doubglas and Linda)... – peleyal Jun 26 '15 at 15:55
  • @DouglasGandini Would you mind sharing a code snippet – Blake Niemyjski Feb 12 '20 at 20:40
  • Sorry @BlakeNiemyjski I don't have access to the source code anymore (It is an old project from a company I worked for). – Douglas Gandini Mar 03 '20 at 19:50
  • I figured it out, you can call `GoogleCredential.FromAccessToken` – Blake Niemyjski Mar 04 '20 at 23:25

1 Answers1

6

Linda helped me with an URL pointing to a new asp.net mvc sample (https://codereview.appspot.com/194980043/).

I just had to add AccessType = "offline" to GoogleOAuth2AuthenticationOptions and save some extra info to create a new instance of TokenResponse when I need.

Google Authentication Options

private GoogleOAuth2AuthenticationOptions ConfigureGooglePlus()
{

    var goolePlusOptions = new GoogleOAuth2AuthenticationOptions()
    {
        AccessType = "offline",
        ClientId = "Xxxxxxx.apps.googleusercontent.com",
        ClientSecret = "Yyyyyyyyyy",
        Provider = new GoogleOAuth2AuthenticationProvider()
        {
            OnAuthenticated = context =>
            {
                context.Identity.AddClaim(new System.Security.Claims.Claim("Google_AccessToken", context.AccessToken));

                if (context.RefreshToken != null)
                {
                    context.Identity.AddClaim(new Claim("GoogleRefreshToken", context.RefreshToken));
                }
                context.Identity.AddClaim(new Claim("GoogleUserId", context.Id));
                context.Identity.AddClaim(new Claim("GoogleTokenIssuedAt", DateTime.Now.ToBinary().ToString()));
                var expiresInSec = (long)(context.ExpiresIn.Value.TotalSeconds);
                context.Identity.AddClaim(new Claim("GoogleTokenExpiresIn", expiresInSec.ToString()));


                return Task.FromResult(0);
            }
        },

        SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie
    };

    goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/plus.login");
    goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/plus.me");
    goolePlusOptions.Scope.Add("https://www.googleapis.com/auth/userinfo.email");

    return goolePlusOptions;
}

How to retrieve the credentials (using token info "stored" as claim)

private async Task<UserCredential> GetCredentialForApiAsync()
{
    var initializer = new GoogleAuthorizationCodeFlow.Initializer
    {
        ClientSecrets = new ClientSecrets
        {
            ClientId = "Xxxxxxxxx.apps.googleusercontent.com",
            ClientSecret = "YYyyyyyyyyy",
        },
        Scopes = new[] { 
        "https://www.googleapis.com/auth/plus.login", 
        "https://www.googleapis.com/auth/plus.me", 
        "https://www.googleapis.com/auth/userinfo.email" }
    };
    var flow = new GoogleAuthorizationCodeFlow(initializer);

    var identity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ApplicationCookie);

    var userId = identity.FindFirstValue("GoogleUserId");

    var token = new TokenResponse()
    {
        AccessToken = identity.FindFirstValue("Google_AccessToken"),
        RefreshToken = identity.FindFirstValue("GoogleRefreshToken"),
        Issued = DateTime.FromBinary(long.Parse(identity.FindFirstValue("GoogleTokenIssuedAt"))),
        ExpiresInSeconds = long.Parse(identity.FindFirstValue("GoogleTokenExpiresIn")),
    };

    return new UserCredential(flow, userId, token);
}
tosc
  • 759
  • 4
  • 16
Douglas Gandini
  • 827
  • 10
  • 24