I have a web application where I am trying to acquire tokens for both Power BI and Microsoft Graph API simultaneously using the below mentioned code snippet.
AuthenticationContext authContext =
new AuthenticationContext(Authority, new NaiveSessionCache(signedInUserID));
//Getting Power BI token
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
code,
new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)),
credential,
pbiResourceID);
//Getting Graph token
AuthenticationResult graphResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
code,
new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)),
credential,
graphResourceId);
The request for acquiring token from Graph API is failing with error:
AADSTS50079: The user is required to use multi-factor authentication
Related
I have created an app in Azure Portal and granted permissions to read user data and user mail. But I get 401 Unauthorized error when trying to get list of mails.
According to the doc, The permissions I need for application are : Mail.ReadBasic.All, Mail.Read, Mail.ReadWrite. I granted all these but still get the error.
In the doc, it's not mentioned whether personal account is supported for this or not in application type permission.
/// get access token
HttpClient httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("https://login.microsoftonline.com/");
var stringContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("client_id", ClientID),
new KeyValuePair<string, string>("scope", "https://graph.microsoft.com/.default"),
new KeyValuePair<string, string>("client_secret", ClientSecret),
new KeyValuePair<string, string>("grant_type", "client_credentials"),
});
var response = httpClient.PostAsync(TenantID + "/oauth2/v2.0/token", stringContent).GetAwaiter().GetResult().Content.ReadAsStringAsync().GetAwaiter().GetResult();
var auth_res_json = JObject.Parse(response);
var token = auth_res_json["access_token"].ToString();
/// now read user data
HttpClient httpClient2 = new HttpClient();
httpClient2.BaseAddress = new Uri("https://graph.microsoft.com/v1.0/");
httpClient2.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response2 = httpClient2.GetAsync("users/" + UserID + "/messages").GetAwaiter().GetResult();
var mails = JObject.Parse(response2.Content.ReadAsStringAsync().GetAwaiter().GetResult());
Note that I get the access token properly, no problem with that.
Also, I can get the user data by using httpClient2.GetAsync("users/" + UserID) properly, no error there too and the results are good.
The permissions I granted are these:
The added scopes are:
And I added these scopes to my application:
Application permissions are consented by the admin for the whole tenant he/she is part of.
Microsoft accounts are not part of a tenant, and as far as I know they don't have a way to consent to application permissions for their sole account.
On top of that when you call the token endpoint, you're providing the tenant ID and not common, this tells the endpoint to only generate tokens valid for this tenant.
You should switch to delegated permissions and depending on what you are building implement the device code flow (native apps) or the on behalf flow (APIs).
Lastly you should look into MSAL and the Microsoft graph SDK librairies, it'll save you a lot of time in implementation.
Background
We developed an application in 2016 that authenticated using WS-Federation, to grab claims from the on-premises AD. The direction of the IT strategy has changed, and is moving toward Azure AD (currently hosting a hybrid environment).
We're in the process of migrating the authentication from WS-Fed, to AAD, using OpenIDConnect. Getting the user signed in and authenticated with the new method was surprisingly straightforward - do the config properly, and issue the authenticate challenge, and Robert is your mother's brother.
The Problem
Please correct me if I'm getting my terminology wrong here; we need to grab some attributes from Active Directory that aren't accessible (as far as I can tell) via the default JWT. So, we need to pass the JWT to the Graph API, via HTTP, to get the attributes we want from active directory.
I know that a properly formatted and authenticated request can pull the necessary data, because I've managed to see it using the graph explorer (the AAD one, not the Microsoft Graph one).
The Question
If my understanding above is correct, how do I pull the JWT from the HttpContext in ASP.Net? If I've grasped all this lower level HTTP stuff correctly, I need to include the JWT in the request header for the Graph API request, and I should get the JSON document I need as a response.
(Edit, for the benefit of future readers: You actually need to acquire a new token for the specific service you're trying to access, in this case Azure AD. You can do this using the on-behalf-of flow, or using the as-an-application flow).
Request.Headers["IdToken"] is returning null, so I'm wondering what's going wrong here.
The Code
Here's our Authentication config that runs on server startup:
public void Configuration(IAppBuilder app)
{
AntiForgeryConfig.SuppressIdentityHeuristicChecks = true;
//ConfigureAuth(app); //Old WsFed Auth Code
//start the quartz task scheduler
//RCHTaskScheduler.Start();
//Azure AD Configuration
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
//sets client ID, authority, and RedirectUri as obtained from web config
ClientId = clientId,
ClientSecret = appKey,
Authority = authority,
RedirectUri = redirectUrl,
//page that users are redirected to on logout
PostLogoutRedirectUri = redirectUrl,
//scope - the claims that the app will make
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.IdToken,
//setup multi-tennant support here, or set ValidateIssuer = true to config for single tennancy
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
SaveSigninToken = true
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed
}
}
);
}
Here's my partially complete code for crafting the GraphAPI request:
public static async Task<int> getEmployeeNumber(HttpContextBase context)
{
string token;
int employeeId = -1;
string path = "https://graph.windows.net/<domain>/users/<AAD_USER_ID>?api-version=1.6";
HttpWebRequest request = null;
request = (HttpWebRequest)HttpWebRequest.Create(path);
request.Method = "GET";
request.Headers.Add(context.GetOwinContext().Request.Headers["IdToken"]);
WebResponse response = await request.GetResponseAsync();
throw new NotImplementedException();
}
Okay it took me a few days to work out (and some pointers from Juunas), but this is definitely doable with some slight modifications to the code here. The aforementioned being the OpenId guide from Microsoft.
I would definitely recommend reading up on your specific authentication scenario, and having a look at the relevant samples.
The above will get you in the door, but to get a JWT from the Graph API, (not to be confused with Microsoft Graph), you need to get an authentication code when you authenticate, and store it in a token cache.
You can get a usable token cache out of this sample from Microsoft (MIT License). Now, personally, I find those samples to be overly obfuscated with complicated use-cases, when really they should be outlining the basics, but that's just me. Nevertheless, these are enough to get you close.
Now for some code. Allow me to draw your attention to the 'ResponseType= CodeIdToken'.
public void ConfigureAuth(IAppBuilder app)
{
//Azure AD Configuration
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
//sets client ID, authority, and RedirectUri as obtained from web config
ClientId = clientId,
ClientSecret = appKey,
Authority = authority,
RedirectUri = redirectUrl,
//page that users are redirected to on logout
PostLogoutRedirectUri = redirectUrl,
//scope - the claims that the app will make
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
//setup multi-tennant support here, or set ValidateIssuer = true to config for single tennancy
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
//SaveSigninToken = true
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
}
}
);
}
When the above parameter is supplied, the following code will run when you authenticate:
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
var code = context.Code;
ClientCredential cred = new ClientCredential(clientId, appKey);
string userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId));
// If you create the redirectUri this way, it will contain a trailing slash.
// Make sure you've registered the same exact Uri in the Azure Portal (including the slash).
Uri uri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(code, uri, cred, "https://graph.windows.net");
}
This will supply your token cache with a code that you can pass to the Graph API. From here, we can attempt to authenticate with the Graph API.
string path = "https://graph.windows.net/me?api-version=1.6";
string tenant = System.Configuration.ConfigurationManager.AppSettings["Tenant"];
string userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string resource = "https://graph.windows.net";
AuthenticationResult result = null;
string authority = String.Format(System.Globalization.CultureInfo.InvariantCulture, System.Configuration.ConfigurationManager.AppSettings["Authority"], tenant);
ClientCredential cc = new ClientCredential(ConfigurationManager.AppSettings["ClientId"], ConfigurationManager.AppSettings["ClientSecret"]);
AuthenticationContext auth = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId));
try
{
result = await auth.AcquireTokenSilentAsync(resource,
ConfigurationManager.AppSettings["ClientId"],
new UserIdentifier(userObjectId, UserIdentifierType.UniqueId)).ConfigureAwait(false);
}
catch (AdalSilentTokenAcquisitionException e)
{
result = await auth.AcquireTokenAsync(resource, cc, new UserAssertion(userObjectId));
}
Once you have the authentication token, you can pass it to the Graph API via Http Request (this is the easy part).
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(path);
request.Method = "GET";
request.Headers.Set(HttpRequestHeader.Authorization, "Bearer " + result.AccessToken);
WebResponse response = request.GetResponse();
System.IO.Stream dataStream = response.GetResponseStream();
From here, you have a datastream that you can pass into a stream reader, get the JSON out of, and do whatever you want with. In my case, I'm simply looking for user data that's in the directory, but is not contained in the default claims that come out of Azure AD Authentication. So in my case, the URL I'm calling is
"https://graph.windows.net/me?api-version=1.6"
If you need to do a deeper dive on your directory, I'd recommend playing with the Graph Explorer. That will help you structure your API calls. Now again, I find the Microsoft documentation a little obtuse (go look at the Twilio API if you want to see something slick). But it's actually not that bad once you figure it out.
EDIT: This question has since gotten a 'notable question' badge from Stack Overflow. Please note, this addresses the ADAL implementation for Azure AD Auth in this scenario. You should be using MSAL, as ADAL is now deprecated! It's mostly the same but there are some key differences in the implementation.
I've created a multi tenant Web API that works just fine. Now I want to build a native client for testing. The Web API app is defined in one tenant (webapitenant). The test app is defined in another tenant (clienttenant) that has given admin consent to the Web API.
I've added the testClientId as a knownClientApplication in the Web API's app manifest and oauth2AllowImplicitFlow enabled. The test client has been granted permissions to the Web API app.
GetAccessToken:
var userCredential = new UserCredential("admin#clienttenant.onmicrosoft.com", "password");
var context = new AuthenticationContext("https://login.windows.net/common");
return context.AcquireToken("https://webapitenant.onmicrosoft.com/webApiResourceUri", testClientId, userCredential).AccessToken;
Exception thrown: 'Microsoft.IdentityModel.Clients.ActiveDirectory.AdalServiceException' in Microsoft.IdentityModel.Clients.ActiveDirectory.dll
Additional information: AADSTS65001: The user or administrator has not consented to use the application with ID 'nativeclientid'. Send an interactive authorization request for this user and resource.
Exception thrown: 'Microsoft.IdentityModel.Clients.ActiveDirectory.AdalServiceException' in Microsoft.IdentityModel.Clients.ActiveDirectory.dll
Additional information: AADSTS65001: The user or administrator has not consented to use the application with ID nativeclientid. Send an interactive authorization request for this user and resource.
Update
I created a dummy console app to force a consent form that I could accept. ADAL now returns tokens but my Web API rejects them (status 401).
var parameters = new PlatformParameters(PromptBehavior.Always);
var context = new AuthenticationContext("https://login.windows.net/common");
var token = context.AcquireTokenAsync
("https://webapi.onmicrosoft.com/appuri",
"testappid",
new Uri("https://webapi.azurewebsites.net"), parameters).Result.AccessToken;
Console.WriteLine(token); //Output: oauth token
var client = new HttpClient
{
BaseAddress = new Uri("https://webapi.azurewebsites.net/api/")
};
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = client.GetAsync("tenants").Result;
Console.WriteLine(response.Content.ReadAsStringAsync().Result);
// Output: {"$type":"System.Web.Http.HttpError, System.Web.Http","Message":"Authorization has been denied for this request."}
Please ensure that the web app is ignore the issue validation and the audience is same as the resource(https://webapi.onmicrosoft.com/appuri",
"testappid) you acquire for the access token and this value should be the App ID URI which you can find it on old Azure portal like figure below:
Here is the relative code for setting for the authentication of multi-tenant web API:
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Audience = ConfigurationManager.AppSettings["ida:Audience"],
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters= new System.IdentityModel.Tokens.TokenValidationParameters {
ValidateIssuer=false
}
});
It looks like if I create a token using "https://management.core.windows.net" as the resource ID then I won't be able to use the same accessToken against the graph API. Is there a way to avoid calling the AcquireTokenAsync more than once?
AuthenticationResult result = authContext.AcquireTokenAsync("https://management.core.windows.net", "<some_client_id>, new Uri("https://localhost"), new PlatformParameters(PromptBehavior.Always)).Result;
TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
tcs.SetResult(result.AccessToken);
ActiveDirectoryClient graphClient = new ActiveDirectoryClient(new Uri("https://graph.windows.net/" + result.TenantId),
() => { return tcs.Task; });
foreach (var app in graphClient.Applications.ExecuteAsync().Result.CurrentPage)
{
Console.WriteLine($"{app.AppId}, {app.DisplayName}");
}
An Azure AD access token is only good for a single resource, so the short answer is no.
However, you do benefit from the fact that Azure AD issues multi-resource refresh tokens, which means that obtaining an access token for the second resource is simply a call to the AAD token endpoint, and not the full authentication flow.
I'm trying to adapt the WebAPI example shown here, to use in MVC5:
https://msdn.microsoft.com/en-US/library/dn931282.aspx#Configure
I have a regular AccountController based login system, but I also need the user to login via OAuth into PowerBI, so I can pull datasets via the PowerBI REST API. However, I'm gettting the ClaimsPrincipal.Current.FindFirst(..) to be null.
private static async Task<string> getAccessToken()
{
// Create auth context (note: token is not cached)
AuthenticationContext authContext = new AuthenticationContext(Settings.AzureADAuthority);
// Create client credential
var clientCredential = new ClientCredential(Settings.ClientId, Settings.Key);
// Get user object id
var userObjectId = ClaimsPrincipal.Current.FindFirst(Settings.ClaimTypeObjectIdentifier).Value;
// Get access token for Power BI
// Call Power BI APIs from Web API on behalf of a user
return authContext.AcquireToken(Settings.PowerBIResourceId, clientCredential, new UserAssertion(userObjectId, UserIdentifierType.UniqueId.ToString())).AccessToken;
}
It all works fine in the sample app (a WebAPI project). I've also configured the OWIN app.UseOpenIdConnectAuthentication stuff in Startup.Auth.cs.
It seems the issue is the only type of Claim I have in 'ClaimsPrincipal.Current' is a 'CookieAuthentication' - it is missing the http://schemas.microsoft.com/identity/claims/objectidentifier Claim.
Also...the Microsoft OAuth window never opens in the browser...however, the error is within the ActiveDirectory related code...that code shouldn't need an OAuth token in the first place, right?
The recommended way to do this is to use the code that the Open ID Connect middleware will automatically retrieve for you. There is relevant sample here:
https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet
This sample uses OAuth to get a token for the AAD Graph API. I don't know PowerBI but I believe that this is exactly analogous to getting a token for PowerBI.
Pay attention in particular to this file:
https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/App_Start/Startup.Auth.cs
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(clientId, appKey);
string userObjectID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);
return Task.FromResult(0);
},
The code above is called on every successful authentication, and ADAL is used to retrieve a token to the Graph API. At this point the only reason to get a token for the Graph API is to exchange the short lived auth code for a longer lived refresh token and get that stored in the cache. That is why the 'result' is never used.
Later, in the following file, the cache is employed to retrieve the token and use it to access the graph:
https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Controllers/UserProfileController.cs
string tenantId = ClaimsPrincipal.Current.FindFirst(TenantIdClaimType).Value;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(Startup.Authority, new NaiveSessionCache(userObjectID));
ClientCredential credential = new ClientCredential(clientId, appKey);
result = authContext.AcquireTokenSilent(graphResourceId, credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
This time the token is actually used.
Substitute PowerBI for Graph API in the sample and I think you should be good to go.
Note that one other thing to pay attention to is the cache implementation. This file contains an appropriately name NaiveSessionCache.
https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs
If you have multiple front ends you will need to implement your own, less naive, session cache so that all the front ends can share the same cache.
A potential workaround, at least for me, is to use the "native app" setup on Azure AD and follow this workflow, instead of the web app + oauth workflow:
https://msdn.microsoft.com/en-US/library/dn877545.aspx