I am using MSAl for Xamarin.Forms and implemented the sample on Xamarin Authorization with Azure AD B2C
In the sample the AcquireTokenSilentAsync()-Method is called from the OnAppearing()-Method of the LoginPage (the View) (delegated from LoginAsync(true)). The login page is the start-up page of this sample app.
My question is, do I have to call AcquireTokenSilentAsync() in any view (or view model) before my logic or is it enough to use it on my start-up page? If I have to use it on any view/view model it seems this is kind of an aspect. Do you solve this by using some AOP pattern or really calling this method on each and every view/view model?
I now call AquireTokenSilentAsync once on startup.
They now have a great explanation how to use it:
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/AcquireTokenSilentAsync-using-a-cached-token
Recommended call pattern in public client applications with Msal 2.x
AuthenticationResult result = null;
var accounts = await app.GetAccountsAsync();
try
{
result = await app.AcquireTokenSilentAsync(scopes, accounts.FirstOrDefault());
}
catch (MsalUiRequiredException ex)
{
// A MsalUiRequiredException happened on AcquireTokenSilentAsync.
// This indicates you need to call AcquireTokenAsync to acquire a token
System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
try
{
result = await app.AcquireTokenAsync(scopes);
}
catch (MsalException msalex)
{
ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
}
}
catch (Exception ex)
{
ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}";
return;
}
if (result != null)
{
string accessToken = result.AccessToken;
// Use the token
}
Related
I'm attempting to acquire a token from AD or Azure AD but my call to AcquireTokenByIntegratedWindowsAuth results in this:
MSAL.Desktop.4.14.0.0.MsalClientException:
ErrorCode: parsing_wstrust_response_failed
Microsoft.Identity.Client.MsalClientException: An error occurred while sending the request.
---> System.Net.Http.HttpRequestException: An error occurred while sending the request.
---> System.Net.WebException: The remote server returned an error: (401) Unauthorized.
---> System.ComponentModel.Win32Exception: The system cannot contact a domain controller to service the authentication request. Please try again later
According to the team that registered my app in Azure I'm a public client and I've got rights to use 'user.read'
Any idea what could be up so that I can communicate something back to our firm's Azure team. It could be my fault, their fault or MS's fault, I'd just like to know who to complain to. Most of the code is generated by the Azure portal, I just changed the call to AcquireTokenInteractive to AcquireTokenByIntegratedWindowsAuth since my final goal is to silently get the token all the time.
public partial class MainWindow : Window
{
string graphAPIEndpoint = "https://graph.microsoft.com/v1.0/me";
string[] scopes = new string[] { "user.read" };
public MainWindow()
{
InitializeComponent();
}
private async void CallGraphButton_Click(object sender, RoutedEventArgs e)
{
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
AuthenticationResult authResult = null;
var app = App.PublicClientApp;
ResultText.Text = string.Empty;
TokenInfoText.Text = string.Empty;
var accounts = await app.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
try
{
authResult = await app.AcquireTokenSilent(scopes, firstAccount)
.ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
try
{
authResult = await app.AcquireTokenByIntegratedWindowsAuth(scopes)
.ExecuteAsync(CancellationToken.None);
}
catch (MsalException msalex)
{
ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
}
}
public partial class App : Application
{
static App()
{
_clientApp = PublicClientApplicationBuilder.Create(ClientId)
.WithAuthority($"{Instance}{Tenant}")
.WithDefaultRedirectUri()
.Build();
TokenCacheHelper.EnableSerialization(_clientApp.UserTokenCache);
}
private static string ClientId = "<My Client ID>";
private static string Tenant = "<Our Tenant ID>";
private static string Instance = "https://login.microsoftonline.com/";
private static IPublicClientApplication _clientApp ;
public static IPublicClientApplication PublicClientApp { get { return _clientApp; } }
}
Based on https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Integrated-Windows-Authentication, there are a number of constraints surrounding the AcquireTokenByIntegratedWindowsAuth method.
If you are testing with your own user account, consent must be granted to the application for your account. Also, 2FA cannot be enabled when using this Auth flow.
For other users, they will need to consent to the application accessing their account details, or the tenant admin must grant consent across the tenant using the Grant admin consent for Tenant button in the portal.
This flow only applies to "federated users" (e.g. created in AD rather than AzureAD).
This flow is targeted primarily at desktop applications. It only works with .net desktop, .net core and Windows Universal Apps.
I'm using the azure mobile services sdk to do offline sync. I made my api so that it is protected with basic authentication using email and password.
How can I embed these credentials with the MobileServiceClient, so that whenever I call a method it has the correct auth credentials.
this is my existing code for the MobileServiceClient.
var handler = new AuthHandler();
//TODO 1: Create our client
//Create our client
MobileService = new MobileServiceClient(Helpers.Keys.AzureServiceUrl, handler)
{
SerializerSettings = new MobileServiceJsonSerializerSettings()
{
CamelCasePropertyNames = true
}
};
//assign mobile client to handler
handler.Client = MobileService;
MobileService.CurrentUser = new MobileServiceUser(Settings.UserId);
MobileService.CurrentUser.MobileServiceAuthenticationToken = Settings.AuthToken;
AuthHandler Class
class AuthHandler : DelegatingHandler
{
public IMobileServiceClient Client { get; set; }
private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(1);
private static bool isReauthenticating = false;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//Clone the request in case we need to send it again
var clonedRequest = await CloneRequest(request);
var response = await base.SendAsync(clonedRequest, cancellationToken);
//If the token is expired or is invalid, then we need to either refresh the token or prompt the user to log back in
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
if (isReauthenticating)
return response;
var service = DependencyService.Get<AzureService>();
var client = new MobileServiceClient(Helpers.Keys.AzureServiceUrl, null);
client.CurrentUser = new MobileServiceUser(Settings.UserId);
client.CurrentUser.MobileServiceAuthenticationToken = Settings.AuthToken;
string authToken = client.CurrentUser.MobileServiceAuthenticationToken;
await semaphore.WaitAsync();
//In case two threads enter this method at the same time, only one should do the refresh (or re-login), the other should just resend the request with an updated header.
if (authToken != client.CurrentUser.MobileServiceAuthenticationToken) // token was already renewed
{
semaphore.Release();
return await ResendRequest(client, request, cancellationToken);
}
isReauthenticating = true;
bool gotNewToken = false;
try
{
gotNewToken = await RefreshToken(client);
//Otherwise if refreshing the token failed or Facebook\Twitter is being used, prompt the user to log back in via the login screen
if (!gotNewToken)
{
gotNewToken = await service.LoginAsync();
}
}
catch (System.Exception e)
{
Debug.WriteLine("Unable to refresh token: " + e);
}
finally
{
isReauthenticating = false;
semaphore.Release();
}
if (gotNewToken)
{
if (!request.RequestUri.OriginalString.Contains("/.auth/me")) //do not resend in this case since we're not using the return value of auth/me
{
//Resend the request since the user has successfully logged in and return the response
return await ResendRequest(client, request, cancellationToken);
}
}
}
return response;
}
private async Task<HttpResponseMessage> ResendRequest(IMobileServiceClient client, HttpRequestMessage request, CancellationToken cancellationToken)
{
// Clone the request
var clonedRequest = await CloneRequest(request);
// Set the authentication header
clonedRequest.Headers.Remove("X-ZUMO-AUTH");
clonedRequest.Headers.Add("X-ZUMO-AUTH", client.CurrentUser.MobileServiceAuthenticationToken);
// Resend the request
return await base.SendAsync(clonedRequest, cancellationToken);
}
private async Task<bool> RefreshToken(IMobileServiceClient client)
{
var authentication = DependencyService.Get<IAuthentication>();
if (authentication == null)
{
throw new InvalidOperationException("Make sure the ServiceLocator has an instance of IAuthentication");
}
try
{
return await authentication.RefreshUser(client);
}
catch (System.Exception e)
{
Debug.WriteLine("Unable to refresh user: " + e);
}
return false;
}
private async Task<HttpRequestMessage> CloneRequest(HttpRequestMessage request)
{
var result = new HttpRequestMessage(request.Method, request.RequestUri);
foreach (var header in request.Headers)
{
result.Headers.Add(header.Key, header.Value);
}
if (request.Content != null && request.Content.Headers.ContentType != null)
{
var requestBody = await request.Content.ReadAsStringAsync();
var mediaType = request.Content.Headers.ContentType.MediaType;
result.Content = new StringContent(requestBody, Encoding.UTF8, mediaType);
foreach (var header in request.Content.Headers)
{
if (!header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))
{
result.Content.Headers.Add(header.Key, header.Value);
}
}
}
return result;
}
}
How can I embed these credentials with the MobileServiceClient, so that whenever I call a method it has the correct auth credentials.
Per my understanding, the AuthHandler class could provide a method for setting the current valid user info after the user has successfully logged in with the correct email and password. Also, you need to cache the AuthHandler instance which is used to construct the MobileServiceClient instance, after user logged, you could embed the current user info into the AuthHandler instance.
If you are talking about providing a sign-in process with a username and password rather than using a social provider, you could just follow Custom Authentication for building your CustomAuthController to work with App Service Authentication / Authorization (EasyAuth). For your client, you could use the following code for logging:
MobileServiceUser azureUser = await _client.LoginAsync("custom", JObject.FromObject(account));
Moreover, you need to cache the MobileServiceAuthenticationToken issued by your mobile app backend and manually valid the cached token and check the exp property of the JWT token under the SendAsync method of your AuthHandler class, and explicitly call LoginAsync with the cached user account for acquiring the new MobileServiceAuthenticationToken when the current token would be expired soon or has expired without asking the user to log in again. Detailed code sample, you could follow adrian hall's book about Caching Tokens.
Or if you are talking about Basic access authentication, you could also refer the previous part about embedding credentials into your AuthHandler. For your server-side, you could also add your custom DelegatingHandler to validate the authorization header and set the related Principal to HttpContext.Current.User. And you could initialize your DelegatingHandler under Startup.MobileApp.cs file as follows:
HttpConfiguration config = new HttpConfiguration();
config.MessageHandlers.Add(new MessageHandlerBasicAuthentication());
Moreover, you could follow Basic Authentication Using Message Handlers In Web API.
I need to authenticate my users using an external API from the login page. If the authentication from the external API succeed then I store at the session a AuthToken.
To check if the request is valid I have created the following Authorization Handler
public class ExtApiStoreRequirement : IAuthorizationRequirement
{
}
public class ExtApiAuthorizationHandler : AuthorizationHandler<ExtApiStoreRequirement>
{
IHttpContextAccessor _accessor;
public ExtApiAuthorizationHandler(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ExtApiStoreRequirement requirement)
{
var authState = GET_AUTH_FROM_SESSION(_accessor.HttpContext.Session);
if (authState!=null)
{
_accessor.HttpContext.Response.Redirect("/Account/Login");
//context.Fail(); <-- I removed that because it was responding an empty page
context.Succeed(requirement);
}
else
context.Succeed(requirement);
return Task.CompletedTask;
}
}
And I have registered this handler at my startup.cs
services.AddAuthorization(options =>
{
options.AddPolicy("ExtApi",
policy => policy.Requirements.Add(new ExtApiStoreRequirement()));
});
This approach is working but I don't feel confident because I have to call context.Succeed(requirement); for the redirection to work. If I call context.Fail() then no redirection takes place and all I see is an empty page.
Is there any security issue with this approach or I will be safe using it?
Your implementation is for authorization not authentication. I think instead of creating an authorization policy, writing custom authentication middleware would be right way for your case.
First see how to implement custom authentication Simple token based authentication/authorization in asp.net core for Mongodb datastore
To implement above way for your case HandleAuthenticateAsync should be something like below:
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
AuthenticateResult result = null;
var principal = GetPrincipalFromSession();
if(principal != null)
{
result = AuthenticateResult.Success(new AuthenticationTicket(principal,
new AuthenticationProperties(), Options.AuthenticationScheme));
}
else
{
result = AuthenticateResult.Skip();
}
return result;
}
Update based on comment:
protected override async Task<bool> HandleUnauthorizedAsync(ChallengeContext context)
{
Response.Redirect(Options.LoginPath);// you need to define LoginPath
return true;
}
Also you should store principal in session when user signs in.
Using above service with Xamarin form, I have enabled authentication with OAuth (Microsoft and Google) at server level.
Call from Swagger works fine. However I'm getting 401 error accessing this via the app. This neither works for TableController nor APIController. I'm not using EasyTables. Following is my code.
public async Task<bool> AuthenticateAsync()
{
bool success = false;
try
{
if (user == null)
{
user = await ItemManager.DefaultManager.CurrentClient.LoginAsync(this, MobileServiceAuthenticationProvider.MicrosoftAccount);
Constants.MobileToken = user.MobileServiceAuthenticationToken;
}
success = true;
}
catch (Exception ex)
{
CreateAndShowDialog(ex.Message, "Authentication failed");
}
return success;
}
public async Task<ObservableCollection<Item>> GetItemsAsync(bool syncItems = false)
{
try
{
IEnumerable<Item> items = await itemTable
.ToEnumerableAsync();
return new ObservableCollection<Item>(items);
}
catch (MobileServiceInvalidOperationException msioe)
{
Debug.WriteLine(#"Invalid sync operation: {0}", msioe.Message);
}
catch (Exception e)
{
Debug.WriteLine(#"Sync error: {0}", e.Message);
}
return null;
}
I tried using rest service client, but not sure how to pass the authentication header. As I seen by Swagger, its actually sending via cookie AppServiceAuthSession. How should it be done via Xamarin Forms?
public ItemManager(IRestService service)
{
restService = service;
}
public Task<List<Item>> GetTasksAsync()
{
return restService.RefreshDataAsync();
}
I read that the token we must supply as the 'X-ZUMO-AUTH' is not the access token that provider send back to us; it is the token that the mobile service backend sends back. How we suppose to retrieve this token? And I don't see Swagger sending X-Zumo-Auth header.
Following is my Rest Service initialization :
public RestService()
{
client = new HttpClient(new LoggingHandler(true));
client.MaxResponseContentBufferSize = 256000;
client.DefaultRequestHeaders.Add("x-access_type", "offline");
client.DefaultRequestHeaders.Add("x-zumo-auth", Constants.MobileToken);
client.DefaultRequestHeaders.Add("ZUMO-API-VERSION", "2.0.0");
}
public async Task<List<Item>> RefreshDataAsync()
{
Items = new List<Item>();
var uri = new Uri(string.Format(Constants.RestUrl, string.Empty));
try
{
var response = await client.GetAsync(uri);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
Items = JsonConvert.DeserializeObject<List<Item>>(content);
}
}
catch (Exception ex)
{
Debug.WriteLine(#" ERROR {0}", ex.Message);
}
return Items;
}
EDIT
After enabling the server logging - Azure service is actually throwing 404 error. And this only happens if I enable the custom authorization on the server.
After debugging the code, I notice following difference between authentication handled by both Mobile App vs Swagger :
Mobile App sets the Authentication Type as Federation, but Swagger is setting it correctly as microsoftaccount
And this makes the ID different as well :
I must not be passing the token correctly here.
So what I figured out so far is that I need to pass the header X-ZUMO-AUTH with the current user token to make it work.
And handle this header in the API code to make retrieve user details
//Try to retrieve from header if available
actionContext.Request.Headers.TryGetValues("x-zumo-auth", out auth_token);
if (auth_token !=null)
{
try
{
string urlPath = string.Concat(new Uri(actionContext.Request.RequestUri, actionContext.Request.GetRequestContext().VirtualPathRoot).AbsoluteUri, ".auth/me");
var result = Get<List<AzureUserDetail>>(HttpWebRequest.Create(urlPath), auth_token.FirstOrDefault(), null)?.FirstOrDefault();
userID = result.User_Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Val;
}
catch
{
actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.NotAcceptable);
}
}
In xamarin forms,RootPage with master detail Layout. My task is to show that page after user successful login. I am using azure mobile service for login. I spend more time to get result.I saw some other solutions but those solution does not render master detail as expected.Finally i got the solution.
Here is the code in app.cs
public App()
{
Client = new MobileServiceClient("your azure url", "your master key");
LoadMainPage();
} public void LoadMainPage()
{
if (Client.CurrentUser == null)
{
MainPage=new NavigationPage(new SplashPage());
}
else
{
MainPage = new RootView();;
}
}
In Login page
async void OnLoginClicked(object sender, EventArgs args)
{
MobileServiceUser user;
try
{
user = await DependencyService.Get<IMobileClient>().LoginAsync(MobileServiceAuthenticationProvider.Facebook);
Application.Current.MainPage=new RootView();
await Navigation.PopToRootAsync();
}
catch (InvalidOperationException ex)
{
if (ex.Message.Contains("Authentication was cancelled"))
{
//messageLabel.Text = "Authentication cancelled by the user";
}
}
catch (Exception ex)
{
// messageLabel.Text = "Authentication failed";
}
}
You need to look at doing navigation, not changing the routes for these paths. Take a look at the Xamarin Navigation docs here: https://developer.xamarin.com/guides/cross-platform/xamarin-forms/getting-started/introduction-to-xamarin-forms/#Navigation
await Navigation.PushModalAsync(new LoginPage());