Sharepoint REST api and MVC AAD connect - c#

My need is to execute this query https://<tenant>.sharepoint.com/_api/search/query?querytext=%27contenttype:articles%27 thru Sharepoint REST api from server side in C#.
I have Oauth2 connection from the MVC portal, so my goal is to retrieve token from connection and send it as bearer token to sharepoint endpoint.
I mean something like that
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);
AuthenticationResult result = await authContext.AcquireTokenSilentAsync("https://<tenant>.sharepoint.com/", credential, new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://<tenant>.sharepoint.com/_api/search/query?querytext=%27contenttype:articles%27");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
but obviously, I can not retrieve the token...
On another side, I have built an app with ADv2 and GraphServiceClient that is working well, but I don't know how to translate the query in graph model (and I don't have any admin-consent).
So, I have 2 ways to resolve my issue, I'll like better use the 2nd option with microsoft graph api, but any help is welcome.
Thank you.

Around Search
The graph search API has limited capabilities, first it will only search in the current site collection (drive) you're targeting, second I'm not sure at the moment it would support a search by content type (maybe with a $filter...)
But it could be an (easier) option if that fits your constraints.
Around auth & auth
In both cases (graph or SharePoint search), what happens when people get to your application (asp.net MVC) is that the authentication middleware takes care of redirecting the user to AAD, get an access token to your app, redirects it to your app which uses that access token to create a session on the app.
My point being: at this point, all you have are:
An access token to your app (not the graph, not SharePoint
A session against your app
You need to do a couple of things to get to SharePoint/the graph:
Intercept and keep the token server side (add it to the session?) if that's not already being done by your implementation of the middlewares
Use that access token + you app id/secret/certificate to get an access token to SharePoint/the graph against AAD
Make sure your application has permissions in AAD to talk to SharePoint/The proper graph API's
Here is a sample on how to get from "I have the access token to my app/api" to "I have an access token to the graph/SharePoint" using MSAL.
Note: I'm using a certificate here, but you could be using a secret instead
var cac = new ClientAssertionCertificate(ApplicationId, CertificateProvider.AppCertificate);
var ua = new UserAssertion(apiAccessToken);
authenticationResult = await authContext.AcquireTokenAsync(resource, cac, ua);
I'm not providing the code on how to intercept the token/get it here because your question is unclear on your current authentication and authorization configuration as well as what MVC "flavor" are you using (asp.net core + middlewares, classic + owin, something else?). I suggest you start another question with more details on that specific point.

Related

How to generate an on-behalf-of token for a middle-tier API

I'm trying to implement an on-behalf-of flow, with Microsoft Identity Platform where my Web app authenticates users, then makes a request to my Web API which in turn makes a request to the Microsoft Graph API (and returns the result to the Web app).
My problem is that I will need to pass on an on-behalf-of token to my Web API for it to be granted acces to Microsoft Graph, but I cannot manage to generate this token. (I'm trying to generate this token using Postman at the moment.)
What I want to be able to run is a code snippet provided by the official documentation here for the Microsoft Graph SDK (in the case of an OBO-flow), and what I need help with is how to generate the token for the oboToken variable.
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.Identity.Client;
var scopes = new[] { "User.Read", "Presence.Read.All" };
var tenantId = "common";
var clientId = "<id of my API as registered in Azure AD / App Registrations>";
var clientSecret = "<value from Client Secret in Registerd Application / Certificates & secrets";
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
var oboToken = "< WHAT NEEDS TO BE PROVIDED BY THE WEB APP >";
var cca = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantId)
.WithClientSecret(clientSecret)
.Build();
var authProvider = new DelegateAuthenticationProvider(async (request) => {
var assertion = new UserAssertion(oboToken);
var result = await cca.AcquireTokenOnBehalfOf(scopes, assertion).ExecuteAsync();
request.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", result.AccessToken);
});
var graphClient = new GraphServiceClient(authProvider);
Console.WriteLine(graphClient.Me.Request().GetAsync().Result);
I've tried the following: I generate an authorization code with the following request in my browser
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
client_id=< Id of my API >
&response_type=code
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp
&response_mode=query
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
Then, with the returned code, make the following POST request (from Postman)
https://login.microsoftonline.com/common/oauth2/v2.0/token?
client_id=< Id of my API >
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp
&grant_type=authorization_code
&code=< code returned by request above >
When I use the returned token from the response as the value of the oboToken in the code snippet, I get a
MsalServiceException: AADSTS50027: JWT token is invalid or malformed.
If I instead also include a client_secret parameter in the POST request above for a token, I get the response AADSTS90023: Public clients can't send a client secret.
How could I generate an on-behalf-of token to be able to run the provided code snippet?
Thanks in advance!
It seems to me that your mistake is getting an MS Graph API token with the authorization code flow.
The way it should work is:
Web App gets an access token to the Web API using authorization code flow
Web API receives the access token and exchanges it for an MS Graph API token using the on-behalf-of flow
Web API calls MS Graph API
So when your Web App gets an access token, it should use a scope defined in the API app registration instead of scope=https%3A%2F%2Fgraph.microsoft.com%2F.default.
If you specify an MS Graph API scope, you get an access token for MS Graph API, meaning you are trying to call MS Graph API from the Web App, instead of your API.

Dynamics CRM api HttpClient Request Authentication with ADFS 3.0

I have an on-premise Dynamics CRM (2016) that is configured with ADFS (3.0). When a user want's to Login, they get redirected to the ADFS login page and the user enter their Windows AD credentials.
From a .net core application I need to make request to the CRM api using HttpClient. When I try to send the credentials like I normally would for a Windows Auth CRM it doesnt work. I get a 401 Unauthorized. Like below.
HttpClient client = new HttpClient(new HttpClientHandler() { Credentials = new NetworkCredential("myuser", "mypassword", "mydomain") });
var result = client.GetAsync("https://mycrmaddress/api/data/v8.0/accounts");
I also tried using Adal to retrieve a token and attach it as a bearer token to the request but I'm unable to get a token with adal. When I try I receive the following:
The authorization server does not support the requested 'grant_type'. The authorization server only supports 'authorization_code'
ADFS 3.0 doesn't support this flow.
I cannot upgrade to ADFS 4.0 so I would like to know what are my options to make an authenticated call to CRM api (without prompting a login window as this application is a service).
Is there any configuration I can do on ADFS so my first example work? Or is it possible to do it with Adal even if it's ADFS 3.0? Or any other solution...
I found the answer to my question. It's kinda hackish, but I tested it myself and it works. As a temporary solution this will do the trick.
Details are available here: https://community.dynamics.com/crm/f/117/t/255985
ADFS 3.0 supports the Authorization Code flow and this what we will use in this case.
We need to retrieve an authorization code. Normally at this steps a windows is prompted to the user to enter its credentials. By doing a POST and sending the user/password it's possible to retrieve an authorization code.
{authProvider} - ADFS Uri - something like
https://adfs.mycompany.com/adfs/oauth2/
{ClientId} - The Guid used to
by your infrastructure team to add your application to ADFS
{RedirectUri} - The IFD Uri for dynamics - should match the redirect
Url used to by your infrastructure team to add your application to
ADFS
username - The User set up on ADFS and in Dynamics
password - The password for the above user
Then we make the following call with these information using HttpClient.
var uri = $"{authProvider}authorize?response_type=code&client_id={clientId}&resource={redirectUri}&redirect_uri={redirectUri}";
var content = new FormUrlEncodedContent(new[] {
new KeyValuePair<string,string>("username",username),
new KeyValuePair<string,string>("password",password),
});
var responseResult = _httpManager.PostAsync(uri, content).Result;
The response content will be an html page (Remember normally this flow prompts a login page to the user). In this page there will be a form that contains the authorization code. using a library like HtmlAgilityPack retrieve the token. This is the hackish part of the solution.
Now that we have an authorization code we need to retrieve an access token.
For that we need to make the following call
var uri = $"{authProvider}token";
var content = new FormUrlEncodedContent(new[] {
new KeyValuePair<string,string>("grant_type","authorization_code"),
new KeyValuePair<string,string>("client_id",clientId),
new KeyValuePair<string,string>("redirect_uri",redirectUri),
new KeyValuePair<string,string>("code",code)
});
var response = await _httpManager.PostAsync(uri, content);
The response content will be a json string that will contain the access token.
With the access token, make the call to CRM rest API.
You will need to attach the token to the HttpClient in the header as a bearer token.
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer",token);
httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0");
From now on you can make calls to CRM api and you will be authorized. However be carefull normally access token are short lived. You will either need to increase their lifetime or request a new token everytime it's expired.

Authenticate with Dynamics 365 from an Azure Function

Scenario
I have a Dynamics 365 v9 organisation hosted online. I have a set of Azure Functions hosted in an Azure Function App on a different tenant to my Dynamics organisation.
I've created web hooks using the Dynamics Plugin Registration Tool, which at certain events (such as when a Contact is created in Dynamics), POST data to my Azure Functions via their endpoint URLs.
Authentication between Dynamics 365 and my Azure Functions is achieved by passing an x-functions-key value in the HTTP request's authentication HttpHeader.
The Azure Functions receive data from the event in Dynamics in the form of a RemoteExecutionContext which I can read using the following code:
using System.Net;
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
var jsonContent = await req.Content.ReadAsStringAsync();
log.Info(jsonContent);
return req.CreateResponse(HttpStatusCode.OK);
}
Question
How can the Azure Function then authenticate back with the calling Dynamics 365 organisation to read and write data?
What I've tried
Xrm Tooling
The simplest way to authenticate would be to use the CrmServiceClient from Microsoft.Xrm.Tooling.Connector.dll. However, I don't necessarily have a username and password to provide the CrmServiceClient's constructor. Perhaps credentials could be passed securely via the HTTP POST request?
Application User
I've tried registering an Application User in Dynamics. I supply the client id and client secret to my Azure Functions, but authentication fails because the user is in a different tenant to my Azure Functions.
Considered Solutions
One object of the received jsonContent string is called ParentContext . Perhaps this can be reused to authenticate back with the calling Dynamics organisation.
Marc Schweigert has recommended using S2S and has provided a sample to his AzureFunctionApp repository. If I can get this approach to work I'll post the solution here.
I wouldn't have thought you can sensibly use the 'real' users credentials to connect to CRM.
I would use a service account to connect back into CRM. Create a new CRM
user especially for this purpose, if you make the user non-interactive you shouldn't consume a license. You can then use the credentials of that service account to connect to CRM using CrmServiceClient. Alternatively have a look at Server to Server authentication.
If you are able to deliver a user id to your Function App, you use the service account to impersonate 'real' users via the CRM web services.
To impersonate a user, set the CallerId property on an instance of
OrganizationServiceProxy before calling the service’s Web methods.
I have done something similar recently, but without relying on the Azure subscription authentication functionality for connecting back into D365. In my case calls were coming to Azure functions from other places, but the connection back is no different. Authentication does NOT pass through in any of these cases. If an AAD user authenticates to your Function application, you still need to connect to D365 using an application user, and then impersonate the user that called you.
First, make sure that the application you registered in Azure AD under App Registrations is of the type "Web app / API" and not "Native". Edit the settings of the registered app and ensure the following:
Take not of the Application ID, which I'll refer to later as appId.
Under "API Access - Required Permissions", add Dynamics CRM Online (Microsoft.CRM) and NOT Dynamics 365.
Under "API Access - Keys", create a key with an appropriate expiry. You can create multiple keys if you have multiple functions/applications connecting back as this "App". I'll refer to this key as "clientSecret" later.
If the "Keys" option isn't available, you've registered a Native app.
I stored the appId and clientSecret in the application configuration section of the Function App, and accessed them using the usual System.Configuration.ConfigurationManager.AppSettings collection.
The below examples use a call to AuthenticationParameters to find the authority and resource URLs, but you could just as easily build those URLs manually using the countless examples online. I find this will just update itself if they ever change, so less work later.
These are simple examples and I'm glossing over the need to refresh tokens and all those things.
Then to access D365 using OData:
string odataUrl = "https://org.crm6.dynamics.com/api/data/v8.2/"; // trailing slash actually matters
string appId = "some-guid";
string clientSecret = "some key";
AuthenticationParameters authArg = AuthenticationParameters.CreateFromResourceUrlAsync(new Uri(odataUrl)).Result;
AuthenticationContext authCtx = new AuthenticationContext(authArg.Authority);
AuthenticationResult authRes = authCtx.AcquireTokenAsync(authArg.Resource, new ClientCredential(appId, clientSecret)).Result;
using (HttpClient client = new HttpClient()) {
client.TimeOut = TimeSpan.FromMinutes (2);
client.DefaultRequestHeaders.Add("Authorization", authRes.CreateAuthorizationHeader ());
using (HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, $"{odataUrl}accounts?$select=name&$top=10")) {
using (HttpResponseMessage res = client.SendAsync(req).Result) {
if (res.IsSuccessStatusCode) {
Console.WriteLine(res.Content.ReadAsStringAsync().Result);
}
else {
// cry
}
}
}
}
If you want to access D365 using the Organization service, and LINQ, use the following. The two main parts that took me a while to find out are the format of that odd looking organization.svc URL, and using Microsoft.Xrm.Sdk.WebServiceClient.OrganizationWebProxyClient instead of Tooling:
string odataUrl = "https://org.crm6.dynamics.com/xrmservices/2011/organization.svc/web?SdkClientVersion=8.2"; // don't question the url, just accept it.
string appId = "some-guid";
string clientSecret = "some key";
AuthenticationParameters authArg = AuthenticationParameters.CreateFromResourceUrlAsync(new Uri(odataUrl)).Result;
AuthenticationContext authCtx = new AuthenticationContext(authArg.Authority);
AuthenticationResult authRes = authCtx.AcquireTokenAsync(authArg.Resource, new ClientCredential(appId, clientSecret)).Result;
using (OrganizationWebProxyClient webProxyClient = new OrganizationWebProxyClient(new Uri(orgSvcUrl), false)) {
webProxyClient.HeaderToken = authRes.AccessToken;
using (OrganizationServiceContext ctx = new OrganizationServiceContext((IOrganizationService)webProxyClient)) {
var accounts = (from i in ctx.CreateQuery("account") orderby i["name"] select i).Take(10);
foreach (var account in accounts)
Console.WriteLine(account["name"]);
}
}
Not sure what context you get back in your Webhook registration, not tried that yet, but just making sure that there's a bearer token in the Authorization header generally does it, and the two examples above inject it in different ways so you should be able to splice together what's needed from here.
This is something I'm curious about as well but I have not had the opportunity to experiment on this.
For your second option have you registered the application and granted consent in the target AAD?
https://learn.microsoft.com/en-us/dynamics365/customer-engagement/developer/use-multi-tenant-server-server-authentication
When they grant consent, your registered application will be added to the Azure AD Enterprise applications list and it is available to the users of the Azure AD tenant.
Only after an administrator has granted consent, you must then create the application user in the subscriber’s Dynamics 365 tenant.
I believe the root of the access issue is related to the Application's Service Principal Object (the Object local to the target Tenant)
Service Principal Object
https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-application-objects#service-principal-object
In order to access resources that are secured by an Azure AD tenant, the entity that requires access must be represented by a security principal. This is true for both users (user principal) and applications (service principal). The security principal defines the access policy and permissions for the user/application in that tenant. This enables core features such as authentication of the user/application during sign-in, and authorization during resource access.
Consider the application object as the global representation of your application for use across all tenants, and the service principal as the local representation for use in a specific tenant.
HTH
-Chris
Using S2S you can use AcquireToken to retrieve the Bearer
var clientcred = new ClientCredential(clientId, clientSecret);
AuthenticationContext authContext = new AuthenticationContext(aadInstance, false);
AuthenticationResult result = authContext.AcquireToken(organizationUrl, clientcred);
token = result.AccessToken;
ExpireDate = result.ExpiresOn.DateTime;
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);

How to implement OAuth 2.0 in ASMX (web-reference)?

I am very new in implementing OAuth 2.0. I have implemented web-reference (ASMX file) in my xamarin project. Now I need to implement OAuth 2.0 for authentication. But I am not sure if ASMX supports OAuth2.0 or not. Do I need to implement WCF instead of Web refernce(ASMX) or is it possible in web reference (ASMX) to implement the OAuth2.0?
One possible solution is to add the token aquired from the authority to the Authorization header.
Your code could look similar to this:
var redirectURI = Windows.Security.Authentication.Web.WebAuthenticationBroker.GetCurrentApplicationCallbackUri();
var _authContext = new AuthenticationContext(authority);
var tokenResult = await _authContext.AcquireTokenAsync(serviceResourceId, clientId, redirectURI);
if (tokenResult.Status != AuthenticationStatus.Success)
{
//Not authenticated
return;
}
var svc = new YourServiceReference.YourClient();
using (var scope = new OperationContextScope(svc.InnerChannel))
{
var httpRequestProperty = new HttpRequestMessageProperty();
httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = tokenResult.AccessToken;
OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
var result = svc.MyFunction();
//Do something with the data
}
Tricky - this is how mobile OAuth solutions work - but it is a big job:
Your Xamarin app would need to implement Mobile SSO to sign users in
Users of your Xamarin app would be redirected to login via an Authorization Server / Identity Provider after which they will receive an OAuth access token
After login your Xamarin App would then be able to call an API with the token and act on behalf of the user to get data
The API would need to validate received access tokens
SOLUTION PARTS
API: ASMX is an old technology so if you are working on the Microsoft stack you would use something newer such as a .Net Core Web API
MOBILE APP: The harder part of the solution will be implementing Mobile SSO, if you have not done so already.
How much of this solution exists already?

Calling Azure AD secured web API with client / secret

I've been at this whole day and it's driving me NUTS. I've got a web API published on Azure which is secured with Azure AD authentication. I've been using this API in combination with a mobile app for quite a while now. On the mobile app, I use the library for client authentication (Microsoft account which is in AD) and this works perfectly. Within the app, API requests are authenticated with the X-ZUMO-AUTH header.
Now for a new part of the application, I've got a C# console app which also needs to be able to call the app. This needs to run stand-alone, so without asking for user credentials. So I created an application in Azure AD, gave it permissions to the Web API instance and got me some code to get an authentication token. That token is then passed into the HttpClient object:
AuthenticationContext ac = new AuthenticationContext(authority);
ClientCredential clientCred = new ClientCredential(clientID, clientSecret);
AuthenticationResult authenticationResult = await ac.AcquireTokenAsync(resource, clientCred);
string authToken = authenticationResult.AccessToken;
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, endpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
HttpResponseMessage response = await client.SendAsync(request);
Server auth is set-up like this:
string aadTenant = configProvider.GetConfigurationSettingValue("ida.Tenant");
string aadAudience = configProvider.GetConfigurationSettingValue("ida.Audience");
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = aadTenant,
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = aadAudience
},
});
I have triple checked all the variables. I also verified that the Bearer token is correctly passed to the service (Fiddler shows it). Everything seems absolutely fine, but still, the server keeps responding with Unauthorized.
My hunch is that this has something to do with the Web API. It's been set up as mobile services back-end. I'm suspecting that this is somehow interfering with 'normal' authentication schemes and the 401 is coming from mobile services not understanding this authentication request, even though the bearer authentication has been setup. But this is just a hunch and I'm not sure whether it actually makes sense or not.
I've seen the related questions on this topic, and all of the samples (used this one as the primary source of inspiration). I've tried all of the variations out there, to no avail. So what I'm looking for is ways to find out where the actual problem is. "Unauthorized" is a bit vague to say the least, so I'm trying to find out why the calls are returned with this response. Any help is greatly appreciated.
Additional endpoint info
As requested, here's some info on the endpoints I'm using:
authority = https://login.microsoftonline.com/{aad-tenant-name}
Also tried the OAuth2 token endpoints listed on the management page:
authority = https://login.microsoftonline.com/{aad-guid}/oauth2/token
authority = https://login.microsoftonline.com/{aad-guid}/oauth2/authorize
For the client, information I'm passing in the ClientID and generated secret I got from the AAD Application page for the application I created. I have granted the application permissions to access the App Service instance for my web API endpoint. Those calls are going to:
https://{app service url}.azurewebsites.net/api/controller
Whilst trying to get this going I noticed more weird behavior. I thought for now I'd just set this particular controller to [AllowAnonymous] so that I could call it without any authentication. But when I do so and omit passing in the Bearer token, I still get Unauthorized as response. That kind of enforces my idea that there's something wrong on the server side and that this might have something to do with how the mobile app stuff is strapped to the webapi controllers.
Never mind, the controller being forced to authorize was caused by Web App level authentication settings, documented here

Categories

Resources