ADAL user consent triggered even when admin has already consented - c#

I've created a Web API which uses Azure Active Directory for its authentication. It uses a multi-tenant AAD. To test it, I also created a console app which uses the ADAL library to authenticate against AAD so I can access my API. In the main AAD tenant all is working well, because I don't need to grant anything. But when accessing the app from a second tenant, I first trigger the admin consent flow (adding a prompt=admin_consent). But when I exit and open the app again, if I try to login with a user with no admin rights on the AAD, it tries to open the user consent and it fails (because the users don't have right to allow access to the AAD). If I already given admin consent, shouldn't the users already be consented?
The code for the test app is:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Authentication;
using System.Threading.Tasks;
using System.Web;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Newtonsoft.Json;
namespace TestConsole
{
internal class Program
{
private const string _commonAuthority = "https://login.microsoftonline.com/common/";
private static void Main(string[] args)
{
ConsoleKeyInfo kinfo = Console.ReadKey(true);
AuthenticationContext ac = new AuthenticationContext(_commonAuthority);
while (kinfo.Key != ConsoleKey.Escape)
{
if (kinfo.Key == ConsoleKey.A)
{
AuthenticationResult ar = ac.AcquireToken("https://babtecportal.onmicrosoft.com/Portal2015.Api", "client_id", new Uri("https://out.es"), PromptBehavior.Auto, UserIdentifier.AnyUser, "prompt=admin_consent");
}
else if (kinfo.Key == ConsoleKey.C)
{
Console.WriteLine("Token cache length: {0}.", ac.TokenCache.Count);
}
else if (kinfo.Key == ConsoleKey.L)
{
ac.TokenCache.Clear();
HttpClient client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, _commonAuthority + "oauth2/logout?post_logout_redirect_uri=" + HttpUtility.UrlEncode("https://out.es"));
var response=client.SendAsync(request).Result;
Console.WriteLine(response.StatusCode);
ac=new AuthenticationContext(_commonAuthority);
}
else
{
int num;
if (int.TryParse(Console.ReadLine(), out num))
{
try
{
AuthenticationResult ar = ac.AcquireToken("https://babtecportal.onmicrosoft.com/Portal2015.Api", "client_id", new Uri("http://out.es"),PromptBehavior.Auto,UserIdentifier.AnyUser);
ac = new AuthenticationContext(ac.TokenCache.ReadItems().First().Authority);
// Call Web API
string authHeader = ar.CreateAuthorizationHeader();
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, string.Format("http://localhost:62607/api/Values?num={0}", num));
request.Headers.TryAddWithoutValidation("Authorization", authHeader);
HttpResponseMessage response = client.SendAsync(request).Result;
if (response.IsSuccessStatusCode)
{
string responseString = response.Content.ReadAsStringAsync().Result;
Values vals = JsonConvert.DeserializeObject<Values>(responseString);
Console.WriteLine("Username: {0}", vals.Username);
Console.WriteLine("Name: {0}", vals.FullName);
vals.Range.ToList().ForEach(Console.WriteLine);
}
else
{
Console.WriteLine("Status code: {0}", response.StatusCode);
Console.WriteLine("Reason: {0}", response.ReasonPhrase);
}
}
catch (AdalException ex)
{
Console.WriteLine(ex.Message);
}
}
}
kinfo = Console.ReadKey(true);
}
}
}
public class Values
{
public string Username { get; set; }
public string FullName { get; set; }
public IEnumerable<int> Range { get; set; }
}
}

Your test app is a native client. In OAuth terms it is a public client. Those terms apply to any client that does not have a client secret or certificate credential of its own. The admin consent feature does not apply to native clients and only works for web applications. Ideally, there would be an error returned when admin consent is attempted for a native app that would indicate that the combination is not supported. We are going to look in to returning such an error in the future to prevent this kind of confusion.
In the meantime, there is no way to prevent users from seeing the consent dialogue when they sign in to a native client.
The situation is somewhat more complicated if the native app is calling a web api where both the native app and web api are owned by the same vendor/tenant. If this is set up correctly then the user will see a combined consent dialog that allows the user to consent to both the native app as well as the web api. The consent to the web api will be recorded permanently. The consent to the native app will only apply to that sign in session in the same way it would if no web api were involved. If a web api is involved in this way then admin consent can be invoked. The admin can then consent to the web api on behalf of all users. However, individual users will still need to consent to the native app.
To correctly set up this consent chain you need to use the 'knownClientApplication' attribute in the application manifest of the web api. You set the value of this attribute to the client id of the native app. You can see this being done in this sample:
https://github.com/AzureADSamples/NativeClient-WebAPI-MultiTenant-WindowsStore/blob/master/README.md
Essentially you download the application manifest through the portal, update this particular value, and then upload it.
There is some more comprehensive documentation on these topics here:
https://msdn.microsoft.com/en-us/library/azure/dn132599.aspx
Update:
One of the stipulations in the above explanation of a native app calling a web api was that they both had to be in the same tenant. If they are not in the same tenant then things get more complicated. This is the case when an ISV has created a web API that they want to make available to apps written by customers. In order for an app to get a token for a resource both apps must be registered in the same tenant. Thus, the first thing the customer will need to do is get the web api registered in their own tenant. If the web api is in the app gallery then they simply go there and install the app. The ISV does not have to have their app in the app gallery to allow customers to register it, but registration gets more complicated. The ISV will need to create a web site, registered in the ISV tenant, that the customer admin can visit. That website needs sign in the admin to get a token for the web api in a way that will trigger the consent process. Once that is complete, then the api will be registered in the customer tenant and available to customer apps.
To get your app in to the app gallery follow the instructions near the bottom of this page:
http://azure.microsoft.com/en-us/marketplace/active-directory/

Related

How to authenticate with Azure Active Directory programatically in a Connect App for Business Central?

I am attempting to write a connect app that will receive a set of data from an external source and put it inside an instance of microsoft dynamics 365 business central via its APIs. Documentation says there are two ways to do this, using basic authentication and logging in via Azure Active Directory. The former is easy and straightforward to do programmatically, but the documentation makes it very clear that it is not meant for production environments. I'm capable of doing the latter using Postman, but part of the process involves me typing in credentials in a popup window. Since the use case for the final product will be to run without user interaction, this won't do. I want the application to handle the credentials of what will be a service account by itself.
I'm able to modify records using basic authentication, and active directory if I fill out the login form when prompted. I've tried using a library called ADAL, but passing my account's credentials that way led to the following response: {"error":"invalid_request","error_description":"AADSTS90014: The request body must contain the following parameter: 'client_secret or client_assertion.}
I have access to the client secret, but there seems to be no means of passing it via ADAL, that I've found.
I've also tried, at a colleague's recommendation, to log in using the client id and client secret as username and password. The following code is what we ended up with:
RestClient client = new RestClient("https://login.windows.net/[my tenant domain]/oauth2/token?resource=https://api.businesscentral.dynamics.com");
var request = new RestRequest(Method.POST);
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("undefined", "grant_type=%20client_credentials&client_id=%20[my client id]&client_secret=[my client secret]&resource=[my resource]", ParameterType.RequestBody);
string bearerToken = "";
try
{
bearerToken = JsonConvert.DeserializeObject<Dictionary<string, string>>(client.Execute(request).Content)["access_token"];
Console.WriteLine(bearerToken);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
The code above successfully retrieves a token, but if I use that token I get the following response:
<error xmlns="http://docs.oasis-open.org/odata/ns/metadata"><code>Unauthorized</code><message>The credentials provided are incorrect</message></error>
I've never used Microsoft dynamics 365. But I've validated an user using a local active directory server using C# code.
using System.DirectoryServices.AccountManagement;
public class ActiveDirectoryService {
// The domain url is the url of the active directory server you're trying to validate with.
public bool ValidateWithActiveDirectoryAsync(string domainUrl, string userName, string password) {
using (var context = new PrincipalContext(ContextType.Domain, domainUrl)) {
UserPrincipal UserPrincipal1 = new UserPrincipal(context);
PrincipalSearcher search = new PrincipalSearcher(UserPrincipal1);
if (context.ValidateCredentials(userName, password)) {
return true;
}
}
return false;
}
}
I hope it works for you.

Should TokenCache be used for offline acces with MSAL?

I am implementing authentication using MSAL and I need some guidance for handling refresh tokens.
My Angular Web App is authenticating with my ASP.NET Web API using MSAL. Web API requires some scopes for accessing Microsoft Graph, so it uses "On Behalf Of" OAuth 2.0 flow to get an access token for calling MS Graph. This part is done and works.
The problem is that MS Graph will be called after some time by my .NET daemon app (using OBO flow) when access token will expire.
What I need is to get refresh token by my Web API and cache it (e.g. in SQL database) so it can be read by daemon app and used to obtain a valid access token.
I suppose that the TokenCache for the confidential client application is the right way to do this but I'm not sure how to get a valid access token by daemon app.
Here is the code of my daemon app I want to use to get access token from AAD:
var userAssertion = new UserAssertion(
<accessToken>,
"urn:ietf:params:oauth:grant-type:jwt-bearer");
var authority = authEndpoint.TrimEnd('/') + "/" + <tenant> + "/";
var clientCredencial = new ClientCredential(<clientSecret>);
var authClient = new ConfidentialClientApplication(<clientId>, authority, <redirectUri>,
clientCredencial, <userTokenCache>, null);
try
{
var authResult =
await authClient.AcquireTokenOnBehalfOfAsync(<scopes>, userAssertion, authority);
activeAccessToken = authResult.AccessToken;
}
catch (MsalException ex)
{
throw;
}
Should I provide <userTokenCache> to get the refresh token form cache? If yes, UserAssertion requires an <accessToken> to be provided, but I don't know what value should be used.
Or should I make a token request on my own and get the refresh token from the response since it is not supported by MSAL? Then I could store the refresh token in the database and use it as <accessToken> with null as <userTokenCache> in daemon app.
I thought it is possible to get the refresh token using MSAL, but I found it is not.
Update
I forgot to say that all of my apps use the same Application ID (this is due to the limitations of the AADv2 endpoint, although I just found that it was removed from the docs at Nov 2nd 2018).
Why not client credentials flow?
Communication with MS Graph could be performed in Web API (using OBO flow) but the task may be delayed by the user, e.g. send mail after 8 hours (Web API will store tasks in the database). The solution for this case is an app (daemon) that runs on schedule, gets tasks from the database and performs calls to MS Graph. I prefer not to give admin consent to any of my apps because it is very important to get consent from the user. If the consent is revoked, call to MS Graph should not be performed. That is why the daemon app should use the refresh token to get access token from AAD for accessing MS Graph (using OBO flow).
I hope it is clear now. Perhaps I should not do it this way. Any suggestion would be appreciated.
MSAL does handle the refresh token itself, you just need to handle the cache serialization. - the userTokenCache is used by the OBO call, and you use the refresh token by calling AcquireTokenSilentAsycn first (that's what refreshes tokens)
- the applicationTokenCache is used by the client credentials flow (AcquireTokenForApplication).
I'd advise you to have a look at the following sample which illustrates OBO: https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore-v2, in particular TodoListService/Extensions/TokenAcquisition.cs#L275-L294
the code is :
var accounts = await application.GetAccountsAsync();
try
{
AuthenticationResult result = null;
var allAccounts = await application.GetAccountsAsync();
IAccount account = await application.GetAccountAsync(accountIdentifier);
result = await application.AcquireTokenSilentAsync(scopes.Except(scopesRequestedByMsalNet), account);
return result.AccessToken;
}
catch (MsalUiRequiredException ex)
{
...
Now the cache is itself initialized from the bearer token that is sent by your client to your Web API. See
TodoListService/Extensions/TokenAcquisition.cs#L305-L336
private void AddAccountToCacheFromJwt(IEnumerable<string> scopes, JwtSecurityToken jwtToken, AuthenticationProperties properties, ClaimsPrincipal principal, HttpContext httpContext)
{
try
{
UserAssertion userAssertion;
IEnumerable<string> requestedScopes;
if (jwtToken != null)
{
userAssertion = new UserAssertion(jwtToken.RawData, "urn:ietf:params:oauth:grant-type:jwt-bearer");
requestedScopes = scopes ?? jwtToken.Audiences.Select(a => $"{a}/.default");
}
else
{
throw new ArgumentOutOfRangeException("tokenValidationContext.SecurityToken should be a JWT Token");
}
var application = CreateApplication(httpContext, principal, properties, null);
// Synchronous call to make sure that the cache is filled-in before the controller tries to get access tokens
AuthenticationResult result = application.AcquireTokenOnBehalfOfAsync(scopes.Except(scopesRequestedByMsalNet), userAssertion).GetAwaiter().GetResult();
}
catch (MsalUiRequiredException ex)
{
...

Generate bearer token client side C#

Im am working with a REST service deployed in an azure environment. I want to run some integration testing by calling various API functions from a separate (console) application. But the REST api uses bearer token authentication. Im a total noob with azure authentications, so i don't even know if it should be possible.
I've tried to use the example found here but no luck yet.
In anycase, I have two applications. One is the console app that is running the code, and the other is the Rest service for which i need to use the bearer token to access the API calls. I will call them the ConsoleApp and RestService.
The code I run is as following:
HttpClient client = new HttpClient();
string tenantId = "<Azure tenant id>";
string tokenEndpoint = $"https://login.microsoftonline.com/{tenantId}/oauth2/token";
string resourceUrl = "<RestService app id url>";
string clientId = "<azure id of the ConsoleApp>";
string userName = "derp#flerp.onmicrosoft.com";
string password = "somepassword";
string tokenEndpoint = $"https://login.microsoftonline.com/{tenantId}/oauth2/token";
var body = $"resource={resourceUrl}&client_id={clientId}&grant_type=password&username={userName}&password={password}";
var stringContent = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded");
var result=await client.PostAsync(tokenEndpoint, stringContent).ContinueWith<string>((response) =>
{
return response.Result.Content.ReadAsStringAsync().Result;
});
JObject jobject = JObject.Parse(result);
The Json message I get back:
error: invalid_grant, error_description: AADSTS50105: The signed in
user is not assigned to a role for the application "RestService
azureid"
What does that mean, and how what needs to be done to get a bearer token out of this?
Please firstly check whether you enabled the User assignment required of console application :
In your azure ad blade ,click Enterprise applications ,search your app in All applications blade ,click Properties :
If enabled that , and your account not assigned access role in your app , then you will get the error . Please try to assign access role in your app :
In your azure ad blade ,click Enterprise applications ,search your console app in All applications blade ,click Users and groups , click Add User button , select your account and assign role(edit user and ensure select role is not None Selected):
Please let me know whether it helps.

Access is Denied on GMAIL API access from Windows Azure (ASP.NET MVC)

I'm facing a problem with GMAIL API from Windows Azure (ASP.NET MVC).
From Local or Console App everything works fine, when I move it to my cloud service I get an "Access is denied" error.
Obviously on the Google Cloud Platform console I have both "localhost" and "myurl" in order to make it work.
This is my code:
UserCredential credential;
using (var stream = new FileStream(diskPath + "/Content/client_secret.json", FileMode.Open, FileAccess.Read))
{
credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
Scopes,
"user",
CancellationToken.None
);
}
var service = new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
var resp = await service.HttpClient.GetStringAsync("https://www.googleapis.com/gmail/v1/users/me/messages?q=\"has:attachment\"");
I get the error on
await GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
Scopes,
"user",
CancellationToken.None
);
I've the last package from NUGET for google API OAuth (https://www.nuget.org/packages/Google.Apis.Oauth2.v2).
I've seen online somebady referring to some access_type=offline parameter but I can't find any "completely working" example.
Why I get this huge difference between local and Azure?
Based from this documentation, you may receive an Access is denied error message when you try to debug a Web application in Visual Studio .NET, and you have administrative permissions.
The problem occurs when the following conditions are true:
You are logged on to your computer with administrative permissions.
You are debugging a Web application in Microsoft Visual Studio .NET.
The operating system that you are using is Microsoft Windows XP Service Pack 2.
The Microsoft ASP.NET worker process account is not a member of the Administrators group.
The problem occurs because the ASP.NET worker process does not have the Impersonate a client after authentication user right. If the worker process account does not have this right, the debugger cannot attach to the process. The worker process account is configured by using the processModel element in the Machine.config file.
You may also check the workaround given in this post. Check if you have added below tag to the web.config file inside system.webserver tag.
<defaultDocument>
<files>
<add value="Pages/Home.aspx"/>
</files>
</defaultDocument>
Finally I find out what was wrong with my solution!
Following this tutorial everything works fine:
https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth#web-applications-aspnet-mvc
Summarizing:
Web applications (ASP.NET MVC)
Google APIs support OAuth 2.0 for Web Server Applications. In order to run the following code successfully, you must first add a redirect URI to your project in the Google API Console. Since you will use FlowMetadata and its default settings, set the redirect URI to your_site/AuthCallback/IndexAsync.
To find the redirect URIs for your OAuth 2.0 credentials, do the following:
Open the Credentials page in the API Console.
If you haven't done so already, create your OAuth 2.0 credentials by clicking Create credentials > OAuth client ID.
After you create your credentials, view or edit the redirect URLs by clicking the client ID (for a web application) in the OAuth 2.0 client IDs section.
After creating a new web application project in your IDE, add the right Google.Apis NuGet package for Drive, YouTube, or the other service you want to use. Then, add the Google.Apis.Auth.MVC package. The following code demonstrates an ASP.NET MVC application that queries a Google API service.
1) Add your own implementation of FlowMetadata.
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "PUT_CLIENT_ID_HERE",
ClientSecret = "PUT_CLIENT_SECRET_HERE"
},
Scopes = new[] { DriveService.Scope.Drive },
DataStore = new FileDataStore("Drive.Api.Auth.Store")
});
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();
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
FlowMetadata is an abstract class that contains your own logic for retrieving the user identifier and the IAuthorizationCodeFlow you are using.
In the above sample code a new GoogleAuthorizationCodeFlow is created with the right scopes, client secrets, and the data store. Consider adding your own implementation of IDataStore, for example you could write one that uses EntityFramework.
2) Implement your own controller that uses a Google API service. The following sample uses a DriveService:
public class HomeController : Controller
{
public async Task IndexAsync(CancellationToken cancellationToken)
{
var result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).
AuthorizeAsync(cancellationToken);
if (result.Credential != null)
{
var service = new DriveService(new BaseClientService.Initializer
{
HttpClientInitializer = result.Credential,
ApplicationName = "ASP.NET MVC Sample"
});
// YOUR CODE SHOULD BE HERE..
// SAMPLE CODE:
var list = await service.Files.List().ExecuteAsync();
ViewBag.Message = "FILE COUNT IS: " + list.Items.Count();
return View();
}
else
{
return new RedirectResult(result.RedirectUri);
}
}
}
3 Implement your own callback controller. The implementation should be something like this:
public class AuthCallbackController : Google.Apis.Auth.OAuth2.Mvc.Controllers.AuthCallbackController
{
protected override Google.Apis.Auth.OAuth2.Mvc.FlowMetadata FlowData
{
get { return new AppFlowMetadata(); }
}
}
This example is for Google Drive but if you want (as I did) to get it working for GMAIL API, you have just to switch between DriveService and GmailService

Azure AD B2C user info Xamarin

Hello I am trying to get user info like name and address in an Xamarin app using Azure AD B2C for authentication.
So far I've gotten the authentication working fine
public async Task<MobileServiceUser> LoginAsync(MobileServiceClient client, MobileServiceAuthenticationProvider provider)
{
try
{
//login and save user status
var user = await client.LoginAsync(Forms.Context, provider);
Settings.AuthToken = user?.MobileServiceAuthenticationToken ?? string.Empty;
Settings.UserId = user?.UserId ?? string.Empty;
return user;
}
catch (Exception e)
{
}
return null;
}
However I would like to know how to get the user's name and birthday. I haven't been able to find a clear course of action for that.
You do not explicitly get this information using the MobileService SDK. Check out the complete documentation about App Service Authentication/Authorization here.
You will reach the point where it mentions:
Your application can also obtain additional user details through an
HTTP GET on the /.auth/me endpoint of your application. A valid token
that's included with the request will return a JSON payload with
details about the provider that's being used, the underlying provider
token, and some other user information. The Mobile Apps server SDKs
provide helper methods to work with this data.
So, in your Xamarin, after the user is successfully authentication, you have to explicitly make a HTTP GET request to /.auth/me and parse the result to get all information about the logged-in user.
Not sure how to do this in Xamarin, but here is how to do it in C# UWP (Universal Windows Platform):
var url = App.MobileService.MobileAppUri + "/.auth/me";
var clent = new Windows.Web.Http.HttpClient();
clent.DefaultRequestHeaders.Add("X-ZUMO-AUTH", this.user.MobileServiceAuthenticationToken);
var userData = await clent.GetAsync(new Uri(url));
at the of this code execution, the userData varibale will be a JSON srting with all user's claims.

Categories

Resources