Search code examples
androidxamaringoogle-oauthamazon-cognitofederated-identity

Using cached Cognito identity from Xamarin


When I first log into my app, I go through the following code:

     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));

Which triggers 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();
  }
}

And finally this code to link the account to 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");
  }

This all works great, and after the login, I am able to use my identity/credentials to retrieve data from DynamoDB. With this object:

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

The second time I run my app, this code runs:

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));

And if I try to use the credentials with DynamoDB (or anything, I assume) at this point, I get errors that I don't have access to the identity. I have to logout (credentials.Clear()) and login again to obtain proper credentials. I could require that a user go through the whole login process every time my app runs, but that's a real pain because the Google login process requires the user to know how to manually close the web browser to get back to the application after authenticating. Is there something I'm missing about the purpose and usage of cached credentials? When I use most apps, they aren't requiring me to log into my Google account every time and close a web browser just to access their server resources.


Solution

  • It looks like the refresh token needs to be submitted back to the OAuth2 provider to get an updated id token to add to the credentials object. First I added some code to save and load the refresh_token in a config.json file:

    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;
       }
    }
    

    Then refactored the code that initiates the interactive login into a separate function:

    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));
    }
    

    Refactored the code that retrieves a Cognito identity into its own function:

    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);
       }
    }
    

    And finally implemented a function that can retrieve a new token based on the refresh token, insert it into the current Cognito credentials, and get an updated Cognito identity.

    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;
       }
    }
    

    I'm not sure if this is optimal or even correct, but it seems to work. I can use the resulting credentials to access DynamoDB without having to prompt the user for permission/credentials again.