I guess my question, Understanding Cognito Identities, wasn't specific enough. I still can't figure out how to use a federated identity from a Xamarin app. Here's what I'm trying, but it's really quite random because I can't find any sample code for this task out there. I tried putting a breakpoint on the AddLogin line, and it never gets hit, even though breakpoint two lines up does get hit. There are too many new-to-me technologies in this code for me to know where to begin on tracking down the problem. (I x'd out the Identity pool ID in the code below, but a real one is there.) At this point I'm just trying to get evidence that I can uniquely identify/validate an Amazon account, and maybe add it to my user pool. But I can't even get the code to entirely execute or report an error.
Login().ContinueWith(t => { if (t.Exception != null)
Toast.MakeText(ApplicationContext, t.Exception.ToString(), ToastLength.Long).Show(); });
public async Task Login()
{
CognitoAWSCredentials credentials = new CognitoAWSCredentials(
"us-east-2:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", // Identity pool ID
RegionEndpoint.USEast2 // Region
);
var client = new Amazon.SecurityToken.AmazonSecurityTokenServiceClient(credentials);
var request = new Amazon.SecurityToken.Model.GetFederationTokenRequest("myamazonid#gmail.com");
var response = await client.GetFederationTokenAsync(request);
credentials.AddLogin("www.amazon.com", response.Credentials.SessionToken);
}
It took a good deal of searching, but I think I figured it out. Setting up the services and getting the client ID is not too hard (is well documented) compared to working out the code, so this answer will focus on the code. Google is particularly tricky because of changes made to their OAuth implementation that prevents some forms of authentication from working. In order for Google identities to work with Cognito, APIs need to be up-to-date. Use NuGet to reference the following API versions or later:
Xamarin.Auth 1.5.0.3
Xamarin.Android.Support.v4 25.4.0.2
Xamarin.Android.Support.CustomTabs 25.4.0.2
AWSSDK.CognitoIdentity 3.3.2.14
AWSSDK.Core 3.3.17.8
Validation 2.4.15
Xamarin.Android.Support.Annotations 25.4.0.2
This code is in the main activity:
protected override void OnCreate(Bundle savedInstanceState)
{
// (etc)
credentials = new CognitoAWSCredentials(
"us-east-2:00000000-0000-0000-0000-000000000000", // Identity pool ID
RegionEndpoint.USEast2 // Region
);
// (etc)
}
private void ShowMessage(string message)
{
AlertDialog dlgAlert = new AlertDialog.Builder(this).Create();
dlgAlert.SetMessage(message);
dlgAlert.SetButton("Close", (s, args) => { dlgAlert.Dismiss(); });
dlgAlert.Show();
}
public void Logout()
{
credentials.Clear();
}
public void Login()
{
if (!string.IsNullOrEmpty(credentials.GetCachedIdentityId()) || credentials.CurrentLoginProviders.Length > 0)
{
if (!bDidLogin)
ShowMessage(string.Format("I still remember you're {0} ", credentials.GetIdentityId()));
bDidLogin = true;
return;
}
bDidLogin = true;
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.mynamespace.myapp:/oauth2redirect"),
new System.Uri("https://www.googleapis.com/oauth2/v4/token"),
isUsingNativeUI: true);
auth.Completed += Auth_Completed;
StartActivity(auth.GetUI(this));
}
private void Auth_Completed(object sender, Xamarin.Auth.AuthenticatorCompletedEventArgs e)
{
if (e.IsAuthenticated)
{
var http = new System.Net.Http.HttpClient();
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:00000000-0000-0000-0000-000000000000";
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");
}
Then there's another activity to handle the callback from the redirect URL in the Google authentication process:
[Activity(Label = "GoodleAuthInterceptor")]
[IntentFilter(actions: new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataSchemes = new[] { "com.mynamespace.myapp" }, 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();
}
}
Related
I've managed to configure my application to authenticate using ADB2C, and it seems to work fine. The ADB2C code implemented is a tweak of one of Microsoft's samples, in which they use a SessionTokenCache class to manage instances of TokenCache. In my application, I retrieve the access token as follows:
private async Task<string> _getAccessToken(IConfidentialClientCredentials credentials)
{
if (this.HasCredentials())
{
var clientCredential = new ClientCredential(credentials.ClientSecret);
var userId = this._getUserIdClaimValue();
var tokenCache = new SessionTokenCache(_httpContextResolver.Context, userId);
var confidentialClientApplication = new ConfidentialClientApplication(
credentials.ClientId,
credentials.Authority,
credentials.RedirectUri,
clientCredential,
tokenCache.GetInstance(),
null);
IAccount account = confidentialClientApplication.GetAccountsAsync().Result.FirstOrDefault();
if (account == null)
{
return "";
}
var authenticationResult = await confidentialClientApplication.AcquireTokenSilentAsync(
credentials.ApiScopes.Split(' '),
account,
credentials.Authority,
false);
return authenticationResult.AccessToken;
}
else
{
return "";
}
}
This method is used to get the access token and pass it in the request header of an HttpClient as follows:
...
using (var request = new HttpRequestMessage(HttpMethod.Get, address.AbsoluteUri))
{
if (this.HasCredentials())
{
string accessToken = await this._getAccessToken(_confidentialClientCredentials);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
using (HttpResponseMessage response = await this.SendAsync(request))
{
//result-processing logic
}
...
The problem is that when the app is restarted, the user remains authenticated through the ADB2C cookie, but confidentialClientApplication.GetAccountsAsync().Result.FirstOrDefault(); returns null. This probably happens because the token cache is destroyed on app restart, so I can probably use a Redis cache to fix.
My main issue however is how to handle the situation of having a null Account but being "authenticated" at the same time. How are my requests to my website being authenticated even though I have a null Account? Shouldn't it fail and redirect me to login page, for example?
I tried looking into Authorization filters, and using the following code to hook up to the auth process and validate if user is null there, but to no avail. The following events are not being called ever (this is in ConfigureServices):
services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("ActiveDirectoryB2C", options))
.AddAzureADB2CBearer(options => Configuration.Bind("ActiveDirectoryB2C", options))
.AddCookie((options) => new CookieAuthenticationOptions
{
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
// context.Principal gives you access to the logged-in user
// context.Properties.GetTokens() gives you access to all the tokens
return Task.CompletedTask;
},
OnSignedIn = context =>
{
return Task.CompletedTask;
}
}
});
It all feels a bit too abstracted for me to make any sense of what's going on. Either that, or I'm missing something fundamental.
Note: The error "Null user was passed in AcquiretokenSilent API. Pass in a user object or call acquireToken authenticate.
" is thrown if I try to pass the null account to the confidentialClientApplication.AcquireTokenSilentAsync() method.
I solved with this code:
protected override void OnException(ExceptionContext filterContext)
{
if (filterContext.Exception is Microsoft.Identity.Client.MsalUiRequiredException)
{
RedirectToAction("SignIn", "Account");
}
else {
//Do your logging
// ...
}
}
I'll search for a better solution.
I am still getting used to Xamarin.Forms and I am on very basic level. I went through many articles for my issue, but to the end couldn't resolve it. So...
Currently I am trying to add Google authentication inside my Xamarin.Forms application, which use Droid and iOS (no WP).
So far I am following guide from here. I am using Xamarin.Auth to authenticate to Google.
Here is some part from my source code.
private async void GoogleSheetsButton_Tapped()
{
string clientId = null;
string redirectUri = null;
if (Device.RuntimePlatform == Device.iOS)
{
clientId = Constants.iOSClientId;
redirectUri = Constants.iOSRedirectUrl;
}
else if (Device.RuntimePlatform == Device.Android)
{
clientId = Constants.AndroidClientId;
redirectUri = Constants.AndroidRedirectUrl;
}
var authenticator = new OAuth2Authenticator(
clientId,
null,
Constants.Scope,
new Uri(Constants.AuthorizeUrl),
new Uri(redirectUri),
new Uri(Constants.AccessTokenUrl),
null,
true);
authenticator.Completed += OnAuthCompleted;
authenticator.Error += OnAuthError;
AuthenticationState.Authenticator = authenticator;
var presenter = new Xamarin.Auth.Presenters.OAuthLoginPresenter();
presenter.Login(authenticator);
}
The problem is coming after my method complete it's work. So after my last line:
presenter.Login(authenticator);
everything looks alright and debugging I am following that compiler goes out of method without any errors, but then I receive exception, which you can see here. It's "No compatible code running".
Here some more information regarding my source code:
Source of "Constants" class used for client ids and URLs
public static class Constants
{
public static string AppName = "....";
// OAuth
// For Google login, configure at https://console.developers.google.com/
public static string iOSClientId = "6.....apps.googleusercontent.com";
public static string AndroidClientId = "6.....apps.googleusercontent.com";
// These values do not need changing
public static string Scope = "https://www.googleapis.com/auth/userinfo.email";
public static string AuthorizeUrl = "https://accounts.google.com/o/oauth2/auth";
public static string AccessTokenUrl = "https://www.googleapis.com/oauth2/v4/token";
public static string UserInfoUrl = "https://www.googleapis.com/oauth2/v2/userinfo";
// Set these to reversed iOS/Android client ids, with :/oauth2redirect appended
public static string iOSRedirectUrl = "com.googleusercontent.apps.6......h:/oauth2redirect";
public static string AndroidRedirectUrl = "com.googleusercontent.apps.6......l:/oauth2redirect";
}
Source of implemented methods for on authentication complete/error, which in fact still I cannot hit because of my error
async void OnAuthCompleted(object sender, AuthenticatorCompletedEventArgs e)
{
var authenticator = sender as OAuth2Authenticator;
if (authenticator != null)
{
authenticator.Completed -= OnAuthCompleted;
authenticator.Error -= OnAuthError;
}
GoogleLoginUser user = null;
if (e.IsAuthenticated)
{
var request = new OAuth2Request("GET", new Uri(Constants.UserInfoUrl), null, e.Account);
var response = await request.GetResponseAsync();
if (response != null)
{
string userJson = await response.GetResponseTextAsync();
user = JsonConvert.DeserializeObject(userJson);
}
if (_account != null)
{
_store.Delete(_account, Constants.AppName);
}
await _store.SaveAsync(_account = e.Account, Constants.AppName);
await DisplayAlert("Email address", user.Email, "OK");
}
}
void OnAuthError(object sender, AuthenticatorErrorEventArgs e)
{
var authenticator = sender as OAuth2Authenticator;
if (authenticator != null)
{
authenticator.Completed -= OnAuthCompleted;
authenticator.Error -= OnAuthError;
}
var message = e.Message;
}
Source of Android MainActivity where I added
public class MainActivity : FormsAppCompatActivity
{
protected override void OnCreate(Bundle bundle)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(bundle);
Forms.Init(this, bundle);
global::Xamarin.Auth.Presenters.XamarinAndroid.AuthenticationConfiguration.Init(this, bundle);
MobileBarcodeScanner.Initialize(Application);
LoadApplication(new App());
}
}
Source of UrlSchemeInterceptorActivity
[Activity(Label = "CustomUrlSchemeInterceptorActivity", NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable }, DataSchemes = new[] { "com.googleusercontent.apps.6......l" }, DataPath = "/oauth2redirect")]
public class CustomUrlSchemeInterceptorActivity : Activity
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
var uri = new Uri(Intent.Data.ToString());
AuthenticationState.Authenticator.OnPageLoading(uri);
Finish();
}
}
Here are the main articles I went through deeply => Link 1, Link 2 and Link 3, but still couldn't resolve the issue.
I am not sure where the error comes from, or can I can I continue debugging it to resolve issue.
Thanks in advance
Solution
Change android compiler to Android 7.0 inside Android project properties. Screenshot
Make sure that inside Android Manifest your target is SDK Version. Screenshot
Update all "Xamarin.Android.*" nuget packages to minimum version 25.4.0.1. Most probably they're currently to 23.3.0. I found problems with dependencies on updating it, so I make manual upload. I went and download manually each package and move it to packages folder. Then I created my own package source and give for path my folder packages and I used it to install already downloaded NuGet packages. Screenshot
After that my issue was solved.
Please update your Android SDK to API 24 or higher and set it as the compile version for your project. And update your referred assemblies for the custom tabs and its dependencies to v25.x.x.
I am developing a Xamarin app (iOS, Android, OSX and Windows) and have to implement calendar synchronization between the app and one user calendar.
This is what I have tried:
Account userAccount;
var auth = new OAuth2Authenticator(
"xxxxxxxxxx.apps.googleusercontent.com", //cliendID
"xxxxxxx", //clientSecret
CalendarService.Scope.Calendar, //scope
new Uri("https://accounts.google.com/o/oauth2/auth"), //authorizationURI
new Uri("https://www.googleapis.com/plus/v1/people/me"), //redirectURI
new Uri("https://accounts.google.com/o/oauth2/token")); //tokeUri
auth.Completed += (sender, e) =>
{
if(e.IsAuthenticated)
{
userAccount = e.Account;
var request = new OAuth2Request(
"GET",
new Uri("https://www.googleapis.com/calendar/v3/users/me/calendarList"),
null,
userAccount);
request.GetResponseAsync().ContinueWith(r =>
{
if(r.IsFaulted)
{
var a = r.Exception.InnerException.Message;
}
else
{
var json = r.Result.GetResponseText();
}
});
}
else
{
//Show message that something failed
}
};
The problem is that e.IsAuthenticated returns true but once I try to get the calendar list to which I pass the userAccount I got in the step before I get the 403 error (e.IsFaulted is true).
Any ideas what I need to do to get the list of calendars?
we have written a WinRT App connected to a Sharepoint 2013.
We are able to authenticate and login to the sharepoint, but we have problems with the logout 'process'. Login is implemented as follows:
We are setting up a HttpClient with the corresponding user credentials and domain information. The configuration is wrapped in the HttpClientConfig class an delivered to a the HttpClientService which holds the HttpClient object.
After that we retrieve the formdigestValue from the sharepoint and use the token in the X-RequestDigest Header in every request. If the token times out we retrieve a new one.
Here is some code how we implemented the above mentioned authentication.
public async Task Inialize()
{
var httpConfig = new HttpClientConfig();
httpConfig.Headers.Add("Accept", "application/json;odata=verbose");
httpConfig.Headers.Add("User-Agent", _userAgent);
httpConfig.DefaultTimeout = Statics.DEFAULT_NETWORK_TIMEOUT_SECONDS;
httpConfig.PreAuthenticate = true;
httpConfig.NetworkCredentials = new NetworkCredential(username, password, _domain);
_httpClientService.ResetCookies();
_httpClientService.ConfigureHttpClient(httpConfig);
}
The ConfigureHttpClient method disposes an old HttpClient instance and creates a new HttpClient instance, like this:
public void ConfigureHttpClient(HttpClientConfig config, bool disposeCurrent = true)
{
_config = config;
if (disposeCurrent)
{
DisposeHttpClient();
}
_httpClient = CreateHttpClient(config);
if (disposeCurrent)
{
//make sure remove old httpclient and httpclienthandler instances after they are not hold anywhere else
GC.Collect();
}
_httpClientDisposed = false;
}
public HttpClient CreateHttpClient(HttpClientConfig config)
{
_httpClientHandler = _httpClientFactoryService.CreateHttpClientHandler();
_httpClientHandler.CookieContainer = _cookieContainer;
_httpClientHandler.UseCookies = true;
_httpClientHandler.AllowAutoRedirect = config.AllowAutoRedirect;
_httpClientHandler.PreAuthenticate = config.PreAuthenticate;
if (config.NetworkCredentials != null)
{
_httpClientHandler.Credentials = config.NetworkCredentials;
}
var client = _httpClientFactoryService.CreateHttpClient(_httpClientHandler, true);
client.Timeout = TimeSpan.FromSeconds(config.DefaultTimeout);
if (config.UseGzipCompression)
{
if (_httpClientHandler.SupportsAutomaticDecompression)
{
_httpClientHandler.AutomaticDecompression = DecompressionMethods.GZip;
client.DefaultRequestHeaders.AcceptEncoding.Add(StringWithQualityHeaderValue.Parse("gzip"));
}
}
return client;
}
public void DisposeHttpClient()
{
var client = _httpClient;
_httpClientDisposed = true; //set flag before disposing is done to be able to react correctly!
if (client != null)
{
client.Dispose();
}
var handler = _httpClientHandler;
if (handler != null)
{
handler.Dispose();
}
GC.Collect();
}
public async Task<object> InitNewSharepointSession(bool useCookies = true)
{
var config = _httpClientService.CurrentClientConfig;
config.UseCookies = useCookies;
var res = await getRequestDigestAsync();
if (res.IsSuccess)
{
SharepointContextInformation = res.Response;
if (config.Headers.ContainsKey("X-RequestDigest"))
{
config.Headers.Remove("X-RequestDigest");
}
config.Headers.Add("X-RequestDigest", SharepointContextInformation.FormDigestValue);
return new DataServiceResponse<bool>(true);
}
else
{
return new DataServiceResponse<bool>(res.Error);
}
}
The ResetCookies method only disposes the old cookies list:
public void ResetCookies()
{
_cookieContainer = new CookieContainer();
}
As you can see we used some GC.Collect() calls which shows a bit our helplessness according the logout stuff.
For logout, we just dispose our httpclient.
But for some reason, if we login with another user, we sometimes get the data of the previous user which is a pretty high rated bug for us.
Everything works nice if we restart the app, but if we only dispose the current users httpClient we may run in this failure having access with the wrong credential/user context of the previous user.
Another thing I watched is the behaviour after a password change. The old password remains and is valid until the app has been restarted.
So I would be very thankful for some hints or suggestions of a sharepoint REST specialist on how to solve this issue.
I guess you are creating a Universal app for Windows 10. In that case, there is no other option than restarting the app, see this answer.
HTTP credentials are not the same as cookies, so resetting the cookies will not help.
However, if you are using System.Net.Http.HttpClient in a Windows 8/8.1 project (no Universal project), disposing the HttpClient should work.
Example with Windows 8/8.1 template. Do NOT use with Universal template.
private async void Foo()
{
// Succeeds, correct username and password.
await Foo("foo", "bar");
// Fails, wrong username and passord.
await Foo("fizz", "buzz");
}
private async Task Foo(string user, string password)
{
Uri uri = new Uri("http://heyhttp.org/?basic=1&user=foo&password=bar");
HttpClientHandler handler = new HttpClientHandler();
handler.Credentials = new System.Net.NetworkCredential(user, password);
HttpClient client = new HttpClient(handler);
Debug.WriteLine(await client.GetAsync(uri));
}
I've spent some time over the last few days trying to implement a feature for my web application. The feature should add new events to a users google calendar while they are offline. I read the Google OAuth2 documentation for web server applications and seem to understand the basics of it. I created a link to authorize the application for offline access:
<a target='_blank' href='https://accounts.google.com/o/oauth2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Ftasks&response_type=code&client_id=<MY CLIENT ID>&access_type=offline&redirect_uri=http%3A%2F%2Flocalhost:49949%2Foauth2callback.aspx'>Grant Tasks Permission</a>
If the user accepts then I capture the refresh token at the redirect uri like this:
private static OAuth2Authenticator<WebServerClient> _authenticator;
protected void Page_Load(object sender, EventArgs e)
{
if (HttpContext.Current.Request["code"] != null)
{
_authenticator = CreateAuthenticator();
_authenticator.LoadAccessToken();
}
Response.Write("Refresh Token: " + _authenticator.State.RefreshToken);
}
private OAuth2Authenticator<WebServerClient> CreateAuthenticator()
{
var provider = new WebServerClient(GoogleAuthenticationServer.Description);
provider.ClientIdentifier = "<MY CLIENT ID>";
provider.ClientSecret = "<MY CLIENT SECRET>";
return new OAuth2Authenticator<WebServerClient>(provider, GetAuthorization);
}
private IAuthorizationState GetAuthorization(WebServerClient client)
{
return client.ProcessUserAuthorization(new HttpRequestInfo(HttpContext.Current.Request));
}
For testing purposes I have been copying the refresh token to a text file for further use.
My problem is using this refresh token for offine access. I have been using this code to refresh the token:
protected void btnGetTasks_Click(object sender, EventArgs e)
{
if (_service == null)
{
_authenticator = CreateAuthenticator();
_service = new TasksService(new BaseClientService.Initializer() { Authenticator = _authenticator });
}
var cl = _service.Tasklists.List().Fetch();
}
private OAuth2Authenticator<WebServerClient> CreateAuthenticator()
{
// Register the authenticator.
var provider = new WebServerClient(GoogleAuthenticationServer.Description);
provider.ClientIdentifier = "<MY CLIENT ID>";
provider.ClientSecret = "<MY CLIENT SECRET>";
var authenticator = new OAuth2Authenticator<WebServerClient>(provider, GetAuthorization);
return authenticator;
}
private IAuthorizationState GetAuthorization(WebServerClient client)
{
string scope = "https://www.googleapis.com/auth/tasks";
IAuthorizationState state = new AuthorizationState(new[] { scope });
state.RefreshToken = "<REFRESH TOKEN FROM FIRST STEP>";
var result = client.RefreshToken(state);
return client.ProcessUserAuthorization();
}
Everything seems fine at this point. When I step through the code I can see the result from client.RefreshToken(state) is true. The issue is when I call this line of code:
_service.Tasklists.List().Fetch();
It returns a (401) unauthorized error from google. I'm looking into the cause but I am not sure how to proceed and I am running short on time with this feature. Any advice would be greatly appreciated. Thanks!
Seems just the act of putting code on here always helps me figure it out a little sooner :)
It now appears this line is unnecessary:
return client.ProcessUserAuthorization();
removing that from the GetAuthorization method and just returning the state passed to RefreshToken has resolved the unauthorized error. I'll leave the question in case it stumps anyone else.