使用来自 Xamarin 的缓存 Cognito 身份

Using cached Cognito identity from Xamarin

当我第一次登录我的应用程序时,我会执行以下代码:

     auth = new Xamarin.Auth.OAuth2Authenticator(
        "my-google-client-id.apps.googleusercontent.com",
        string.Empty,
        "openid",
        new System.Uri("https://accounts.google.com/o/oauth2/v2/auth"),
        new System.Uri("com.enigmadream.storyvoque:/oauth2redirect"),
        new System.Uri("https://www.googleapis.com/oauth2/v4/token"),
        isUsingNativeUI: true);

     auth.Completed += Auth_Completed;
     StartActivity(auth.GetUI(this));

触发这个 activity:

[Activity(Label = "GoodleAuthInterceptor")]
[IntentFilter(actions: new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
  DataSchemes = new[] { "com.enigmadream.storyvoque" }, DataPaths = new[] { "/oauth2redirect" })]
public class GoodleAuthInterceptor : Activity
{
  protected override void OnCreate(Bundle savedInstanceState)
  {
     base.OnCreate(savedInstanceState);
     Android.Net.Uri uri_android = Intent.Data;
     Uri uri_netfx = new Uri(uri_android.ToString());
     MainActivity.auth?.OnPageLoading(uri_netfx);
     Finish();
  }
}

最后这个代码 link 到 Cognito 的帐户:

  private void Auth_Completed(object sender, Xamarin.Auth.AuthenticatorCompletedEventArgs e)
  {
     if (e.IsAuthenticated)
     {
        var idToken = e.Account.Properties["id_token"];

        credentials.AddLogin("accounts.google.com", idToken);
        AmazonCognitoIdentityClient cli = new AmazonCognitoIdentityClient(credentials, RegionEndpoint.USEast2);
        var req = new Amazon.CognitoIdentity.Model.GetIdRequest();
        req.Logins.Add("accounts.google.com", idToken);
        req.IdentityPoolId = "us-east-2:79ebf8e1-97de-4d1c-959a-xxxxxxxxxxxx";
        cli.GetIdAsync(req).ContinueWith((task) =>
        {
           if ((task.Status == TaskStatus.RanToCompletion) && (task.Result != null))
           {
              ShowMessage(string.Format("Identity {0} retrieved", task.Result.IdentityId));
           }
           else
              ShowMessage(task.Exception.InnerException != null ? task.Exception.InnerException.Message : task.Exception.Message);
        });
     }
     else
        ShowMessage("Login cancelled");
  }

这一切都很好,登录后,我可以使用 identity/credentials 从 DynamoDB 检索数据。有了这个对象:

Amazon.DynamoDBv2.AmazonDynamoDBClient ddbc = new Amazon.DynamoDBv2.AmazonDynamoDBClient(credentials, RegionEndpoint.USEast2);

我第二次 运行 我的应用程序,此代码 运行s:

if (!string.IsNullOrEmpty(credentials.GetCachedIdentityId()) || credentials.CurrentLoginProviders.Length > 0)
{
   if (!bDidLogin)
   {
      var idToken = credentials.GetIdentityId();
      ShowMessage(string.Format("I still remember you're {0} ", idToken));

如果此时我尝试将凭据与 DynamoDB(或我假设的任何东西)一起使用,我会收到错误消息,指出我无权访问该身份。我必须注销 (credentials.Clear()) 并再次登录以获取正确的凭据。 我可以要求用户在每次我的应用 运行 时都完成整个登录过程,但这真的很痛苦,因为 Google 登录过程要求用户知道如何手动关闭网络浏览器以验证后返回应用程序。关于缓存凭据的用途和用法,我是否遗漏了什么?当我使用大多数应用程序时,它们并不要求我每次都登录我的 Google 帐户并关闭网络浏览器来访问它们的服务器资源。

看起来需要将刷新令牌提交回 OAuth2 提供程序以获取更新的 ID 令牌以添加到凭据对象。首先,我添加了一些代码以在 config.json 文件中保存和加载 refresh_token:

private Dictionary<string, string> config;
const string CONFIG_FILE = "config.json";

private void Auth_Completed(object sender, Xamarin.Auth.AuthenticatorCompletedEventArgs e)
{
   if (e.IsAuthenticated)
   {
      var idToken = e.Account.Properties["id_token"];
      if (e.Account.Properties.ContainsKey("refresh_token"))
      {
         if (config == null)
            config = new Dictionary<string, string>();
         config["refresh_token"] = e.Account.Properties["refresh_token"];
         WriteConfig();
      }

      credentials.AddLogin("accounts.google.com", idToken);
      CognitoLogin(idToken).ContinueWith((t) =>
      {
         try
         {
            t.Wait();
         }
         catch (Exception ex)
         {
            ShowMessage(ex.Message);
         }
      });
   }
   else
      ShowMessage("Login cancelled");
}

void WriteConfig()
{
   using (var configWriter = new System.IO.StreamWriter(
      Application.OpenFileOutput(CONFIG_FILE, Android.Content.FileCreationMode.Private)))
   {
      configWriter.Write(ThirdParty.Json.LitJson.JsonMapper.ToJson(config));
      configWriter.Close();
   }
}

public void Login()
{
   try
   {
      if (!string.IsNullOrEmpty(credentials.GetCachedIdentityId()) || credentials.CurrentLoginProviders.Length > 0)
      {
         if (!bDidLogin)
         {
            var idToken = credentials.GetIdentityId();
            if (ReadConfig())
            {
               LoginRefreshAsync().ContinueWith((t) =>
               {
                  try
                  {
                     t.Wait();
                     if (!t.Result)
                        FullLogin();
                  }
                  catch (Exception ex)
                  {
                     ShowMessage(ex.Message);
                  }
               });
            }
            else
            {
               credentials.Clear();
               FullLogin();
            }
         }
      }
      else
         FullLogin();
      bDidLogin = true;
   }
   catch(Exception ex)
   {
      ShowMessage(string.Format("Error logging in: {0}", ex.Message));
   }
}

private bool ReadConfig()
{
   bool bFound = false;
   foreach (string filename in Application.FileList())
      if (string.Compare(filename, CONFIG_FILE, true) == 0)
      {
         bFound = true;
         break;
      }
   if (!bFound)
      return false;
   using (var configReader = new System.IO.StreamReader(Application.OpenFileInput(CONFIG_FILE)))
   {
      config = ThirdParty.Json.LitJson.JsonMapper.ToObject<Dictionary<string, string>>(configReader.ReadToEnd());
      return true;
   }
}

然后将启动交互式登录的代码重构为一个单独的函数:

public void FullLogin()
{
   auth = new Xamarin.Auth.OAuth2Authenticator(CLIENTID_GOOGLE, string.Empty, "openid",
      new Uri("https://accounts.google.com/o/oauth2/v2/auth"),
      new Uri("com.enigmadream.storyvoque:/oauth2redirect"),
      new Uri("https://accounts.google.com/o/oauth2/token"),
      isUsingNativeUI: true);

   auth.Completed += Auth_Completed;
   StartActivity(auth.GetUI(this));
}

将检索 Cognito 身份的代码重构到它自己的函数中:

private async Task CognitoLogin(string idToken)
{
   AmazonCognitoIdentityClient cli = new AmazonCognitoIdentityClient(credentials, RegionEndpoint.USEast2);
   var req = new Amazon.CognitoIdentity.Model.GetIdRequest();
   req.Logins.Add("accounts.google.com", idToken);
   req.IdentityPoolId = ID_POOL;
   try
   {
      var result = await cli.GetIdAsync(req);
      ShowMessage(string.Format("Identity {0} retrieved", result.IdentityId));
   }
   catch (Exception ex)
   {
      ShowMessage(ex.Message);
   }
}

并最终实现了一个功能,可以根据刷新令牌检索新令牌,将其插入当前 Cognito 凭证,并获得更新的 Cognito 身份

private async Task<bool> LoginRefreshAsync()
{
   string tokenUrl = "https://accounts.google.com/o/oauth2/token";
   try
   {
      using (System.Net.Http.HttpClient client = new System.Net.Http.HttpClient())
      {
         string contentString = string.Format(
            "client_id={0}&grant_type=refresh_token&refresh_token={1}&",
            Uri.EscapeDataString(CLIENTID_GOOGLE),
            Uri.EscapeDataString(config["refresh_token"]));
         System.Net.Http.HttpContent content = new System.Net.Http.ByteArrayContent(
            System.Text.Encoding.UTF8.GetBytes(contentString));
         content.Headers.Add("content-type", "application/x-www-form-urlencoded");
         System.Net.Http.HttpResponseMessage msg = await client.PostAsync(tokenUrl, content);
         string result = await msg.Content.ReadAsStringAsync();
         string idToken = System.Json.JsonValue.Parse(result)["id_token"];
         credentials.AddLogin("accounts.google.com", idToken);
         /* EDIT -- discovered this is not necessary! */
         // await CognitoLogin(idToken);
         return true;
      }
   }
   catch (Exception ex)
   {
      ShowMessage(ex.Message);
      return false;
   }
}

我不确定这是否最佳甚至正确,但它似乎有效。我可以使用生成的凭据访问 DynamoDB,而无需再次提示用户输入 permission/credentials。

有一个非常不同的解决方案,我正在尝试适应其他答案。但它是如此不同,我将其添加为一个单独的答案。

问题似乎与需要显式使用刷新令牌来获取更新的访问令牌没有太大关系(我认为这是隐式完成的),而是需要记住身份令牌。因此,无需包括手动应用刷新令牌的所有复杂性,所需要做的只是存储身份令牌(这可以通过类似于存储刷新令牌的方式来完成)。然后我们只需要在丢失时将相同的身份令牌添加回凭证对象。

if (!string.IsNullOrEmpty(credentials.GetCachedIdentityId()) || credentials.CurrentLoginProviders.Length > 0)
{
   if (config.Read())
   {
      if (config["id_token"] != null)
         credentials.AddLogin(currentProvider.Name, config["id_token"]);

编辑:需要使用刷新令牌的问题仍然存在。此代码在令牌未过期时有效,但在令牌过期后尝试使用这些凭据将失败,因此在某些情况下仍然需要以某种方式使用刷新令牌。