Is getting list of all mails supported with personal account in Microsoft Graph? - microsoft-graph-api

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.

Related

Mix MSAL and MVC Forms Authentication

I'm working on a single tenant MVC web app with multiple users. There are multiple deployments of this app for different customers and some customers would like to send mail via Outlook 365. The app uses forms authentication with users stored in the database.
I've attempted to get something working based on the following sample - https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect - however it conflicts with the forms authentication in that I get looped back to the forms login page even if log in is successful.
I can successfully retrieve an access token by directing the user to the Azure AD sign in after successful login:
var url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?" +
"client_id=" + AuthenticationConfig.ClientId +
"&response_type=code" +
"&redirect_uri=" + AuthenticationConfig.RedirectUri +
"&response_mode=query" +
"&scope=" + AuthenticationConfig.BasicSignInScopes +
"&state=12345";
Then in the callback use the following to get the access token:
public static async Task<string> GetAccessToken(string code)
{
var app = ConfidentialClientApplicationBuilder
.Create(AuthenticationConfig.ClientId)
.WithClientSecret(AuthenticationConfig.ClientSecret)
.WithRedirectUri(AuthenticationConfig.RedirectUri)
.WithAuthority(AuthenticationConfig.Authority)
.Build();
var scopes = new List<string> { "offline_access", "user.read", "mail.read", "mail.send" };
var authenticationResult = await app.AcquireTokenByAuthorizationCode(scopes, code).ExecuteAsync();
return authenticationResult.AccessToken;
}
However no refresh token is returned in the authentication result. I'd hoped to get the access token/expiry and refresh token/expiry and store them so the user would authenticate once via Azure AD and then I could silently renew the access token using the refresh token in the future.
I've done a lot of reading but most of the information I can find is based on using Azure AD login for your app. I need to retain the forms login and authenticate for the purpose sending email only. Is this possible?
UPDATE to attempt to cache the token against the user and read it back:
public static async Task OnAuthorisationCodeReceived(string code)
{
var app = ConfidentialClientApplicationBuilder
.Create(AuthenticationConfig.ClientId)
.WithClientSecret(AuthenticationConfig.ClientSecret)
.WithRedirectUri(AuthenticationConfig.RedirectUri)
.WithAuthority(AuthenticationConfig.Authority)
.Build();
var scopes = new List<string> { "offline_access", "user.read", "mail.read", "mail.send" };
var authenticationResult = await app.AcquireTokenByAuthorizationCode(scopes, code).ExecuteAsync();
var account = authenticationResult.Account;
var identity = new ClaimsIdentity();
identity.AddClaim(new System.Security.Claims.Claim(ClaimConstants.ObjectId, account.HomeAccountId.ObjectId));
identity.AddClaim(new System.Security.Claims.Claim(ClaimConstants.TenantId, account.HomeAccountId.TenantId));
identity.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Upn, account.Username));
var claimsPrincipal = new ClaimsPrincipal(identity);
// as per example store the cache
var userTokenCache = new MSALPerUserMemoryTokenCache(app.UserTokenCache, claimsPrincipal);
// no accounts returned
var accounts = await app.GetAccountsAsync();
var newres = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault()).ExecuteAsync().ConfigureAwait(false);
}
There are no accounts returned, the only way I can see cached tokens is if I leave the default so they're not stored against the user.
Unfortunately, MSAL.NET (and MSALs in general) doesn't expose the refresh token.
You can see that there is not "refreshToken" in AuthenticationResult.
you can handle refreshing token automatically by calling AcquireTokenSilent.

To retrieve access token

I have created a MVC application to escalate work to other person inside my organization. I have added all the members in my organization to AAD,
and registered an application there, created app service and linked that app service to registered app with SSO enabled.
Now every time someone visits the app, they can login successfully using their respective credential.
What I want to do know is to retrieve all the members in my AAD and display them inside dropdown list so that anyone can escalate to others by just looking in the dropdown list.
I have tried with sample graph SDK to get the name of users in my organization
with this code
private string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private string appId = ConfigurationManager.AppSettings["ida:AppId"];
private string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private string scopes = ConfigurationManager.AppSettings["ida:GraphScopes"];
public async Task<string> GetUserAccessTokenAsync()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
HttpContextWrapper httpContext = new HttpContextWrapper(HttpContext.Current);
TokenCache userTokenCache = new SessionTokenCache(signedInUserID, httpContext).GetMsalCacheInstance();
//var cachedItems = tokenCache.ReadItems(appId); // see what's in the cache
ConfidentialClientApplication cca = new ConfidentialClientApplication(
appId,
redirectUri,
new ClientCredential(appSecret),
userTokenCache,
null);
try
{
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes.Split(new char[] { ' ' }), cca.Users.First());
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (Exception)
{
HttpContext.Current.Request.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
throw new ServiceException(
new Error
{
Code = GraphErrorCode.AuthenticationFailure.ToString(),
Message = Resource.Error_AuthChallengeNeeded,
});
}
}
with some change in scope.
<add key="ida:AppId" value="xxxxx-xxxxxxx-xxxxxx-xxxxxxx"/>
<add key="ida:AppSecret" value="xxxxxxxxxxx"/>
<add key="ida:RedirectUri" value="http://localhost:55065/"/>
<add key="ida:GraphScopes" value="User.ReadBasic.All User.Read Mail.Send Files.ReadWrite"/>
This enables me to get basic details of all user in my organization.
But how I can achieve this in my app where authentication related stuffs are done in azure only, and there is no code for authentication and authorization in entire solution.
Thanks
Subham, NATHCORP, INDIA
But how I can achieve this in my app where authentication related stuffs are done in azure only, and there is no code for authentication and authorization in entire solution.
Based on my understanding, you are using the build-in feature App Service Authentication / Authorization. You could follow here to configure your web app to use AAD login. And you need to configure the required permissions for your AD app as follows:
Note: For Azure AD graph, you need to set the relevant permissions for the Windows Azure Active Directory API. For Microsoft Graph, you need to configure the Microsoft Graph API.
Then, you need to configure additional settings for your web app. You could access https://resources.azure.com/, choose your web app and update App Service Auth Configuration as follows:
Note: For using Microsoft Graph API, you need to set the resource to https://graph.microsoft.com. Details, you could follow here.
For retrieving the access token in your application, you could get it from the request header X-MS-TOKEN-AAD-ACCESS-TOKEN. Details, you could follow Working with user identities in your application.
Moreover, you could use Microsoft.Azure.ActiveDirectory.GraphClient package for Microsoft Azure Active Directory Graph API, Microsoft.Graph package for Microsoft Graph API using the related access token.

MVC App using Azure AD with ADAL 3 - Authentication Cookie expires after 1 hour

I work on an MVC Web Application using Azure AD with OAuth 2 and Open ID Connect for Authorization of users.
Per documentation tokens are refreshed automatically when a token expires after 60 minutes (which is fine).
Now the problem is, to acquire a token I need to know the currently authenticated user which is stored in a cookie. The code to acquire a Token is like this:
public async Task<AuthenticationToken> GetTokenForApplication(string resourceID)
{
string signedInUserID = ClaimsPrincipal.Current.SignedinUserId();
var tenantID = ClaimsPrincipal.Current.TenantId();
string userObjectID = ClaimsPrincipal.Current.SignedinUserObjectId();
// get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
ClientCredential clientcred = new ClientCredential(Config.ClientId, Config.AppKey);
// initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
AuthenticationContext authenticationContext = new AuthenticationContext(string.Format("{0}{1}", Config.AadInstance, tenantID), new ADALTokenCache(signedInUserID));
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenSilentAsync(resourceID, clientcred, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
var token = new AuthenticationToken(authenticationResult.AccessToken) { ExpiresOn = authenticationResult.ExpiresOn };
return token;
}
Now I am in the dilemma, that the ClaimsPrincipal.Current.SignedinUserId() method call throws a null reference exception. When I inspect the ClaimsPrincipal.Current object, no data about the logged in user is available. But this is the Information needed to renew / request a token.
What is the best practice in an MVC Web App? Is there a way to extend the validity of the cookie or is there any way to reauthenticate the current user without redirecting to the root page of the web application?
After doing more research I have found these two pages which describe some options to deal with my problem pretty good:
Controlling a Web App’s session duration
and ASP.NET-Identity-Cookie-Authentication-Timeouts
are these good approaches?
After doing more research I have found these two pages which describe some options to deal with my problem pretty good:
Controlling a Web App’s session duration
and ASP.NET-Identity-Cookie-Authentication-Timeouts
are these good approaches?

How to get access token for web api from native app in other tenant?

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

MVC5 app using Azure Active Directory + REST API -- to auth for PowerBI / O365

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

Resources