Как библиотека Google API V 3.0 .Net и Google OAuth2 обрабатывают токен обновления

В моем приложении я использую библиотеку Google API V 3.0 .Net с Google OAuth2 для синхронизации календаря Google и календаря Outlook. Я использую приведенный ниже код для получения объекта службы Google.Apis.Calendar.v3.CalendarService. Во время аутентификации я сохранил файл Json, после чего запрашиваю объект Google.Apis.Auth.OAuth2.UserCredential.

private Google.Apis.Auth.OAuth2.UserCredential GetGoogleOAuthCredential()
{
    GoogleTokenModel _TokenData = new GoogleTokenModel();
    String JsonFilelocation = "jsonFileLocation;
    Google.Apis.Auth.OAuth2.UserCredential credential = null;
    using (var stream = new FileStream(JsonFilelocation, FileMode.Open,
                    FileAccess.Read))
    {
        Google.Apis.Auth.OAuth2.GoogleWebAuthorizationBroker.Folder = "Tasks.Auth.Store";
        credential = Google.Apis.Auth.OAuth2.GoogleWebAuthorizationBroker.AuthorizeAsync(
        Google.Apis.Auth.OAuth2.GoogleClientSecrets.Load(stream).Secrets,
        new[] { Google.Apis.Calendar.v3.CalendarService.Scope.Calendar },
        "user",
        CancellationToken.None,
        new FileDataStore("OGSync.Auth.Store")).Result;
    }
    return credential;
}

Запрос на код объекта службы:

Google.Apis.Calendar.v3.CalendarService _V3calendarService = new Google.Apis.Calendar.v3.CalendarService(new Google.Apis.Services.BaseClientService.Initializer()
{
HttpClientInitializer = GetGoogleOAuthCredential(),
ApplicationName = "TestApplication",
});

Приведенный выше код прекрасно работает для получения объекта Calendarservice. Мой вопрос в том, что в моем файле Json есть токены обновления и доступа. как приведенный выше код обрабатывает токен обновления, чтобы получить службу, когда токен истек? Поскольку мне нужно часто вызывать объект Calendarservice, мне нравится реализовывать шаблон синглтона для объекта calenderService. Как получить Calendarservice без частого вызова GetGoogleOAuthCredential? Любая помощь / руководство приветствуется.

3 ответа

Потратил последние два дня, чтобы понять это сам. Библиотека не обновляет токены автоматически, если вы не укажете "access_type = offline".

https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth

Я вставлю код, который использую, и если есть что-то, чего вы не понимаете, просто спросите. Я прочитал очень много постов, и сейчас я получил эту работу, поэтому есть некоторый закомментированный код, и он еще не был реорганизован. Надеюсь, это кому-нибудь поможет. Я использую пакеты NuGet:

Google.Apis.Auth.MVC

Google.Apis.Calendar.v3

Код:

AuthCallbackController:

[AuthorizationCodeActionFilter]
public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
{
protected static readonly ILogger Logger = ApplicationContext.Logger.ForType<AuthCallbackController>();

/// <summary>Gets the authorization code flow.</summary>
protected IAuthorizationCodeFlow Flow { get { return FlowData.Flow; } }

/// <summary>
/// Gets the user identifier. Potential logic is to use session variables to retrieve that information.
/// </summary>
protected string UserId { get { return FlowData.GetUserId(this); } }

/// <summary>
/// The authorization callback which receives an authorization code which contains an error or a code.
/// If a code is available the method exchange the coed with an access token and redirect back to the original
/// page which initialized the auth process (using the state parameter).
/// <para>
/// The current timeout is set to 10 seconds. You can change the default behavior by setting 
/// <see cref="System.Web.Mvc.AsyncTimeoutAttribute"/> with a different value on your controller.
/// </para>
/// </summary>
/// <param name="authorizationCode">Authorization code response which contains the code or an error.</param>
/// <param name="taskCancellationToken">Cancellation token to cancel operation.</param>
/// <returns>
/// Redirect action to the state parameter or <see cref="OnTokenError"/> in case of an error.
/// </returns>
[AsyncTimeout(60000)]
public async override Task<ActionResult> IndexAsync(AuthorizationCodeResponseUrl authorizationCode,
   CancellationToken taskCancellationToken)
{
    if (string.IsNullOrEmpty(authorizationCode.Code))
    {
        var errorResponse = new TokenErrorResponse(authorizationCode);
        Logger.Info("Received an error. The response is: {0}", errorResponse);
        Debug.WriteLine("Received an error. The response is: {0}", errorResponse);
        return OnTokenError(errorResponse);
    }

    Logger.Debug("Received \"{0}\" code", authorizationCode.Code);
    Debug.WriteLine("Received \"{0}\" code", authorizationCode.Code);


    var returnUrl = Request.Url.ToString();
    returnUrl = returnUrl.Substring(0, returnUrl.IndexOf("?"));

    var token = await Flow.ExchangeCodeForTokenAsync(UserId, authorizationCode.Code, returnUrl,
        taskCancellationToken).ConfigureAwait(false);

    // Extract the right state.
    var oauthState = await AuthWebUtility.ExtracRedirectFromState(Flow.DataStore, UserId,
        authorizationCode.State).ConfigureAwait(false);

    return new RedirectResult(oauthState);
}

protected override Google.Apis.Auth.OAuth2.Mvc.FlowMetadata FlowData
{
    get { return new AppFlowMetadata(); }
}

protected override ActionResult OnTokenError(TokenErrorResponse errorResponse)
{
    throw new TokenResponseException(errorResponse);
}


//public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
//{
//    protected override Google.Apis.Auth.OAuth2.Mvc.FlowMetadata FlowData
//    {
//        get { return new AppFlowMetadata(); }
//    }
//}

}

Метод для вызова контроллером Google API

    public async Task<ActionResult> GoogleCalendarAsync(CancellationToken cancellationToken)
    {
        var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).
            AuthorizeAsync(cancellationToken);

        if (result.Credential != null)
        {
            //var ttt = await result.Credential.RevokeTokenAsync(cancellationToken);

            //bool x = await result.Credential.RefreshTokenAsync(cancellationToken);

            var service = new CalendarService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = result.Credential,
                ApplicationName = "GoogleApplication",
            });
            var t = service.Calendars;

            var tt = service.CalendarList.List();

            // Define parameters of request.
            EventsResource.ListRequest request = service.Events.List("primary");
            request.TimeMin = DateTime.Now;
            request.ShowDeleted = false;
            request.SingleEvents = true;
            request.MaxResults = 10;
            request.OrderBy = EventsResource.ListRequest.OrderByEnum.StartTime;

            // List events.
            Events events = request.Execute();
            Debug.WriteLine("Upcoming events:");
            if (events.Items != null && events.Items.Count > 0)
            {
                foreach (var eventItem in events.Items)
                {
                    string when = eventItem.Start.DateTime.ToString();
                    if (String.IsNullOrEmpty(when))
                    {
                        when = eventItem.Start.Date;
                    }
                    Debug.WriteLine("{0} ({1})", eventItem.Summary, when);
                }
            }
            else
            {
                Debug.WriteLine("No upcoming events found.");
            }


            //Event myEvent = new Event
            //{
            //    Summary = "Appointment",
            //    Location = "Somewhere",
            //    Start = new EventDateTime()
            //        {
            //            DateTime = new DateTime(2014, 6, 2, 10, 0, 0),
            //            TimeZone = "America/Los_Angeles"
            //        },
            //    End = new EventDateTime()
            //        {
            //            DateTime = new DateTime(2014, 6, 2, 10, 30, 0),
            //            TimeZone = "America/Los_Angeles"
            //        },
            //    Recurrence = new String[] {
            //        "RRULE:FREQ=WEEKLY;BYDAY=MO"
            //        },
            //    Attendees = new List<EventAttendee>()
            //        {
            //        new EventAttendee() { Email = "johndoe@gmail.com" }
            //        }
            //};

            //Event recurringEvent = service.Events.Insert(myEvent, "primary").Execute();

            return View();
        }
        else
        {
            return new RedirectResult(result.RedirectUri);
        }
    }

Производный класс FlowMetadata

public class AppFlowMetadata : FlowMetadata
    {
        //static readonly string server = ConfigurationManager.AppSettings["DatabaseServer"];
        //static readonly string serverUser = ConfigurationManager.AppSettings["DatabaseUser"];
        //static readonly string serverPassword = ConfigurationManager.AppSettings["DatabaseUserPassword"];
        //static readonly string serverDatabase = ConfigurationManager.AppSettings["DatabaseName"];
    ////new FileDataStore("Daimto.GoogleCalendar.Auth.Store")
    ////new FileDataStore("Drive.Api.Auth.Store")
    //static DatabaseDataStore databaseDataStore = new DatabaseDataStore(server, serverUser, serverPassword, serverDatabase);


    private static readonly IAuthorizationCodeFlow flow =
new ForceOfflineGoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
    ClientSecrets = new ClientSecrets
    {
        ClientId = "yourClientId",
        ClientSecret = "yourClientSecret"

    },
    Scopes = new[]
    {
CalendarService.Scope.Calendar, // Manage your calendars
//CalendarService.Scope.CalendarReadonly // View your Calendars
    },
    DataStore = new EFDataStore(),
});

    public override string GetUserId(Controller controller)
    {
        // In this sample we use the session to store the user identifiers.
        // That's not the best practice, because you should have a logic to identify
        // a user. You might want to use "OpenID Connect".
        // You can read more about the protocol in the following link:
        // https://developers.google.com/accounts/docs/OAuth2Login.

        //var user = controller.Session["user"];
        //if (user == null)
        //{
        //    user = Guid.NewGuid();
        //    controller.Session["user"] = user;
        //}
        //return user.ToString();

        //var store = new UserStore<ApplicationUser>(new ApplicationDbContext());
        //var manager = new UserManager<ApplicationUser>(store);
        //var currentUser = manager.FindById(controller.User.Identity.GetUserId());

        return controller.User.Identity.GetUserId();

    }

    public override IAuthorizationCodeFlow Flow
    {
        get { return flow; }
    }

    public override string AuthCallback
    {
        get { return @"/GoogleApplication/AuthCallback/IndexAsync"; }
    }
}

Entity Framework 6 Класс хранилища данных

 public class EFDataStore : IDataStore
    {
        public async Task ClearAsync()
        {
            using (var context = new ApplicationDbContext())
            {
                var objectContext = ((IObjectContextAdapter)context).ObjectContext;
                await objectContext.ExecuteStoreCommandAsync("TRUNCATE TABLE [Items]");
            }
        }

        public async Task DeleteAsync<T>(string key)
        {
            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException("Key MUST have a value");
            }

        using (var context = new ApplicationDbContext())
        {
            var generatedKey = GenerateStoredKey(key, typeof(T));
            var item = context.GoogleAuthItems.FirstOrDefault(x => x.Key == generatedKey);
            if (item != null)
            {
                context.GoogleAuthItems.Remove(item);
                await context.SaveChangesAsync();
            }
        }
    }

    public Task<T> GetAsync<T>(string key)
    {
        if (string.IsNullOrEmpty(key))
        {
            throw new ArgumentException("Key MUST have a value");
        }

        using (var context = new ApplicationDbContext())
        {
            var generatedKey = GenerateStoredKey(key, typeof(T));
            var item = context.GoogleAuthItems.FirstOrDefault(x => x.Key == generatedKey);
            T value = item == null ? default(T) : JsonConvert.DeserializeObject<T>(item.Value);
            return Task.FromResult<T>(value);
        }
    }

    public async Task StoreAsync<T>(string key, T value)
    {
        if (string.IsNullOrEmpty(key))
        {
            throw new ArgumentException("Key MUST have a value");
        }

        using (var context = new ApplicationDbContext())
        {
            var generatedKey = GenerateStoredKey(key, typeof(T));
            string json = JsonConvert.SerializeObject(value);

            var item = await context.GoogleAuthItems.SingleOrDefaultAsync(x => x.Key == generatedKey);

            if (item == null)
            {
                context.GoogleAuthItems.Add(new GoogleAuthItem { Key = generatedKey, Value = json });
            }
            else
            {
                item.Value = json;
            }

            await context.SaveChangesAsync();
        }
    }

    private static string GenerateStoredKey(string key, Type t)
    {
        return string.Format("{0}-{1}", t.FullName, key);
    }
}

Производный класс для GoogleAuthorizationCodeFlow. Включение долгоживущего токена обновления, обеспечивающего автоматическое "обновление" токена, что просто означает получение нового токена доступа.

https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth

internal class ForceOfflineGoogleAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
    public ForceOfflineGoogleAuthorizationCodeFlow(GoogleAuthorizationCodeFlow.Initializer initializer) : base (initializer) { }

    public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri)
    {
        return new GoogleAuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl))
        {
            ClientId = ClientSecrets.ClientId,
            Scope = string.Join(" ", Scopes),
            RedirectUri = redirectUri,
            AccessType = "offline",
            ApprovalPrompt = "force"
        };
    }
}

GoogleAuthItem используется с EFDataStore

public class GoogleAuthItem
{
    [Key]
    [MaxLength(100)]
    public string Key { get; set; }

    [MaxLength(500)]
    public string Value { get; set; }
}

public DbSet<GoogleAuthItem> GoogleAuthItems { get; set; }

Это большая часть клиентской библиотеки! это волшебство делается для вас автоматически:)

UserCredential реализует как IHttpExecuteInterceptor, так и IHttpUnsuccessfulResponseHandler. поэтому всякий раз, когда срок действия токена доступа истекает или уже истек, клиент обращается к серверу авторизации, чтобы обновить токен и получить новый токен доступа (который действителен в течение следующих 60 минут).

Подробнее об этом читайте на https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth.

Для меня клиентская библиотека не выполняла обновление, даже не создавала токен обновления.
Я проверяю, истек ли токен, и обновляю. (Жетон имеет 1 час жизни). credential.Token.IsExpired сообщит вам, истек ли он, credential.Token.RefreshToken(идентификатор пользователя) обновит необходимый токен.

Другие вопросы по тегам