We're moving most of our web presence to our SharePoint server in the cloud. Our current setup uses a MVC Web API for data retrieval from DB. We do not want to host the API under a separate domain and thus need to move the API under SharePoint domain as well. There is no relaxation in this requirement.
Is there a way to publish my API to SharePoint? Or is there a SharePoint specific API project template in Visual Studio? If not what are my options?
EDIT Initially I have asked that MVC API needs to be part of the SharePoint 2013. But now things are such that API can reside anywhere - inside or outside - of SharePoint, as long as it is accessible from the root domain - which so far it seems not allowed (Error message: Calls to WebProxy without an app context are not allowed."). Still trying to see if this is possible, and if yes, how?
It sounds like the proxy you want to create is already part of SharePoint JSOM. Have a look at these:
http://msdn.microsoft.com/en-us/library/office/fp179895(v=office.15).aspx
http://msdn.microsoft.com/en-us/library/office/jj245162(v=office.15).aspx
This will allow you to overcome cross origin issues. The SP.WebProxy and SP.WebRequestInfo allow you to use javascript to make a call outside of the domain where the javascript executes.
What really happens behind the scenes is that SharePoint's javascript API sends the request to your sharepoint.com tenancy server, which will then invoke the service from the SharePoint server, and return the response back to your javascript. You can implement it like so in a sharepoint-hosted app:
// this javascript executes from my-company.sharepoint.com
var responseDocument = undefined;
$('#cross').click(function () {
var ctx = SP.ClientContext.get_current();
var request = new SP.WebRequestInfo();
request.set_url('https://www.somewebapi.com/my/custom/route');
request.set_method("GET");
responseDocument = SP.WebProxy.invoke(ctx, request); // executes on sp server
ctx.executeQueryAsync(onSuccess, onError);
});
function onSuccess() {
var response = responseDocument.get_body();
alert('success ' + response);
}
function onError(err) {
alert(JSON.stringify(err));
}
...and since the remote api hosted at the other domain is called from the server, you don't have to worry about any of the cross-domain issues.
Update
To answer your update, please check the results from this link.
Have you added the remote endpoint to your AppManifest.xml?
SharePoint doesn't give you a chance to define you own routes. Thats why you can not use old fashioned SharePoint solution to publish asp.net web api. You may consider using apps for SharePoint. It's like separate App with some connections to SharePoint.
Ultimately switched to JSONP solution. Installed the WebApiContrib.Formatting.JsonP in my MVC Web API project in Visual Studio, and modified SharePoint JavaScript, that calls the API, to include ?callback=? (callback is equal to question mark). Everything stays the same. No SharePoint's proxy caller needed! No SharePoint app needed!
Related
We use the Microsoft.SharePoint.Client library to automate SharePoint work from our workflow engine but yesterday, one of our client informed us they wanted to disable the Legacy Authentication (LegacyAuthProtocolsEnabled to false).
Once I tried it on our end, I ended up getting an Unauthorised exception.
All in good wanting to disable the Legacy Authentication for obvious security reason, but the problem with the Modern Authentication is that it requires user interaction which is clearly not a solution since we are running tasks in the background.
I've been googling this for quite some time but I haven't found a solution as of yet on how to handle automatic authentication for background work.
Is there a way to "authenticate" to SharePoint without any user interaction while LegacyAuthProtocolsEnabled is set to false?
I found an article that suggested using the App Authentication but after reading more about it, I believe this is considered an old method to authenticate and is likely to be deprecated as well over time, but I thought I'd still give it a go just in case but it did not work. When I got to
https://tenant.sharepoint.com/_layouts/15/appregnew.aspx
Where tenant is our company domain name, and I click on the "Create" button after filling in all the relevant fields, I get the following error, which is completely useless:
Sorry, something went wrong
An unexpected error has occurred.
TECHNICAL DETAILS
According to this article HOW TO HARDEN YOUR SHAREPOINT ONLINE ENVIRONMENT BY DISABLING LEGACY AUTHENTICATION, Legacy Authentication was no longer be an option as of the 13/10/2020, yet here we are, and the option is still available in SharePoint 365 and while the article is interesting explain why Legacy Authentication should be switched off, etc... it does not get into any details as to how automated solutions should be handled.
Also found an old thread "LegacyAuthProtocolsEnabled" and Scripted Logons to SharePoint Online? where #DeanWang suggests leaving it turned on as:
All custom CSOM, PowerShell code will stop working
This may also prevent third-party apps from accessing SharePoint
Online resources.
I'm going to stop here as I could keep going and the question is already too long for my liking and bottom line is, does anyone know if there is a way, and what is the best way, to authenticate to SharePoint while running automated "scripts/code" from a background task without requiring any user interaction while the Legacy Authentication is switch off?
Thanks
Update-1
After reading articles after articles, I've yet to connect to SharePoint 365.
I also spend more time on the PnP Framework as recommended by numerous articles. I created a dummy app with the following sample code which is used again in various articles, including this one:
Secure Authentication of SharePoint with PnP Framework with C#(Code)
My code is identical as you can see:
var clientContext = new AuthenticationManager().GetACSAppOnlyContext(
"https://mycompany.sharepoint.com/sites",
"MyClientid",
"MySecretId");
using (clientContext)
{
//Get Lists
var web = clientContext.Web;
var lists = web.Lists;
clientContext.Load(lists);
clientContext.ExecuteQuery();
foreach (var list in lists)
{
}
}
And even though I've granted full control in Azure for the specific test app that's using the specific ClientId and SecretId
I'm still getting the following error (401 - unauthorized):
System.Exception
HResult=0x80131500
Message=Token request failed.
Source=PnP.Framework
StackTrace:
at SharePointPnP.IdentityModel.Extensions.S2S.Protocols.OAuth2.OAuth2S2SClient.Issue(String securityTokenServiceUrl, OAuth2AccessTokenRequest oauth2Request) in /_/src/lib/PnP.Framework/Utilities/OAuth/OAuth2S2SClient.cs:line 18
at PnP.Framework.Utilities.TokenHelper.GetAppOnlyAccessToken(String targetPrincipalName, String targetHost, String targetRealm) in /_/src/lib/PnP.Framework/Utilities/TokenHelper.cs:line 116
at PnP.Framework.Utilities.ACSTokenGenerator.GetToken(Uri siteUrl) in /_/src/lib/PnP.Framework/Utilities/ACSTokenGenerator.cs:line 37
at PnP.Framework.AuthenticationManager.<GetContextAsync>b__59_0(String site) in /_/src/lib/PnP.Framework/AuthenticationManager.cs:line 971
at PnP.Framework.AuthenticationManager.<>c__DisplayClass75_0.<GetAccessTokenContext>b__0(Object sender, WebRequestEventArgs args) in /_/src/lib/PnP.Framework/AuthenticationManager.cs:line 1336
at Microsoft.SharePoint.Client.ClientRuntimeContext.OnExecutingWebRequest(WebRequestEventArgs args)
at Microsoft.SharePoint.Client.ClientContext.FireExecutingWebRequestEventInternal(WebRequestEventArgs args)
at Microsoft.SharePoint.Client.ClientContext.GetWebRequestExecutor()
at Microsoft.SharePoint.Client.ClientContext.GetFormDigestInfoPrivate()
at Microsoft.SharePoint.Client.ClientContext.EnsureFormDigest()
at Microsoft.SharePoint.Client.ClientContext.ExecuteQuery()
at ConsoleApp5.Program.Main(String[] args) in C:\Users\myuser\source\repos\ConsoleApp5\ConsoleApp5\Program.cs:line 23
This exception was originally thrown at this call stack:
[External Code]
SharePointPnP.IdentityModel.Extensions.S2S.Protocols.OAuth2.OAuth2WebRequest.GetResponse() in OAuth2WebRequest.cs
SharePointPnP.IdentityModel.Extensions.S2S.Protocols.OAuth2.OAuth2S2SClient.Issue(string, SharePointPnP.IdentityModel.Extensions.S2S.Protocols.OAuth2.OAuth2AccessTokenRequest) in OAuth2S2SClient.cs
Inner Exception 1:
WebException: The remote server returned an error: (401) Unauthorized.
Is there another section I should be looking at (and change) in the App Registration in Azure
Since it's the SharePoint Online that we are talking about, one easy way to connect to different SharePoint Sites is by using the Azure AD App-Only approach and since you are talking about a Deamon Service you can easily use Application Permissions when registering the App Registration.
You can, and you should, read more about it from the linked Microsoft Docs article.
You can also loggin via certificate or app registration secret as it is discribed in the Log in to Microsoft 365 in order to create automated CI CD SPFx pipelines, for example.
Hope the above helps, if not feel free to ask :)
Update: Please read below in order to have a better understanding.
Firstly, in your code segment you are using a wrong method from the PnP.Framework package.
AuthenticationManager().GetACSAppOnlyContext()
The above method refers to a completely different method of obtaining an authentication token, more specifically the Sharepoint App-Only model, which... well.... more or less is not being used nowadays quite so ofte. I think I read somewhere that MS is thinking of retiring this kind of Authentication and going onwards on the path of Azure Active Directory authentication, but, unfotunately, I cannot seem to find the link.
Furthermore, I have collected three projects and uploaded them to github for you to see. You can simply clone the repo and run the projects as-is from HERE.
As you will be able to see for yourself, there are three projects in the solution, which you can run each one individually from VSCode or Vs.
More in detail:
ConsoleApp1
(sorry for the name but forgot to switch it :) )
This is a Deamon Console Project that references the PnP.Framework namespace and tries to utilize all of the goodies that the good folks form the PnP Community have contributed.
The procedure is straight forward and is the same for all three projects ->
Read the AppConfiguration
Request the Access Token with appropriate scopes (Depending the service that i am referencing)
Declare the Token to be used by our Client Context.
In the PnP.Framework-related project the above cycle can be seen as below
AuthenticationConfiguration config = AuthenticationConfiguration.ReadFromJsonFile("appsettings.json");
var authManager = new PnP.Framework.AuthenticationManager(config.ClientId, config.Certificate.CertificateDiskPath, config.Certificate.CertificatePassword, config.Tenant);
using (var cc = authManager.GetAccessTokenContext("https://<REPLACE:name of tenant>.sharepoint.com/sites/testsite2", (string siteURL) => authManager.GetAccessToken(siteURL)))
ConsoleAppMSGraph
As the name suggests this Deamon Console App utilizes GraphServiceClient graphClient in order to get all the information that you request through the graph endpoint.
Subsequntly, you will notice that for this porject the scope name changes to
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
In addition, we request a collection of all the lists that currently reside in our SharePoint Root Site with the below segment:
var lists = await graphClient.Sites["root"].Lists
.Request()
.GetAsync();
ConsoleAppSPClient
This app is the default and most simple way of accessing data on Sharepoint.
The projects utilizes MSAL.Net and Microsoft.Sharepoint.Client namespaces in order to fetch an access token and, subsequently, embed that token in all our next requests.
In order to keep the answer a bit short, please refer to here in order to see how we initiate a Confidential App Client, request for a token and, later on, embedd it in our ClientContext object.
Notes
I have listed in the Readme.md of the repo, which permissions you should give to your app registration. You can view them Here.
I am using the Sites.FullControl.All but you can narrow down the list of sites that the app registration will have access by using the Sites.Selected.
All of the above projects, reference a common class library that serves as a strongly typed configuration object.
IMPORTANT you should always use a certificate to authenticate the client app as it is mentioned here. The previous link also describes the way you can create a certificate and upload it to the store of the app registration.
Amazing! Thank you very much #Jimas13. For the last 2 weeks I was struggling to find solution to my problem!! You saved me!! If you ever been in Greece let me buy you a drink!
I created a new Web API project and created the following routing spec (actually I have simplified, looking for the bug):
// Web API configuration and services
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}"
);
When I make a call via ajax with the route:
/api/account/GetSUID/0
everything works fine. When I just use a different controller:
/api/tile/GetTileSet/0
it returns a status of 200 but instead of hitting my controller, it just returns the contents of the default page in jqXHR.responseText! It is as if it is just skipping any API routing like I am requesting the default site page.
I am baffled by this one as I have written literally hundreds of web API functions over the past few years in several other projects. I have never had any issue making calls to multiple controllers. I have looked high and low for what could be happening here and am hoping that someone here might have an idea.
Here is a sample method on the controller:
[HttpGet]
public HttpResponseMessage CheckRequestedID(int id, [FromUri]string Search)
{
if (!BSDIUtil.HasAllAcceptableCharacters(Search))
return Request.CreateResponse(HttpStatusCode.BadRequest);
if (FolderModel.IDAlreadyExists(DAL, Search)) // We can check this because this function is only called when staff members are creating accounts for other people (participants always use their email).
return Request.CreateResponse(HttpStatusCode.OK, false);
else
return Request.CreateResponse(HttpStatusCode.OK, true);
}
This will work if on the account controller but not on the tile controller.
One other thing, I am using the "Community" edition of Visual Studio and Windows 8.1
This is not a problem that is likely to occur often but I have solved it and figured I would post it here in case anyone else has the same issue.
I am using web api in the context of a standard web forms app (although I am only using webforms for my reporting pages). In the web.config for a web forms app, you can declare the paths that the user has access to before authenticating. I was only providing access to the account controller: all others were not permitted due to my authentication mechanism. Once I authenticate (e.g. the forms authentication call) or if I change the location path to include only "api", the problem goes away.
I was facing same problem but in different context. Had many controllers and respective routing templates. Only one controller was responding to requests.
Later i realized my other controller classes were not public!!
My problem is simple. I have a registered Sharepoint site/domain (say https://secretText-my.sharepoint.com/personal/blabla) and I want to fetch the changelogs as described here Sharepoint Change log
So my question boils down to >>> How can I use this Changelog API to fetch data for a remote Sharepoint site?
How can I achieve this? I have tried Client Object Model and everything related but my goal is to use Sharepoint Change log.
I am hoping for something like,
using (ClientContext ctx = ClaimClientContext.GetAuthenticatedContext("https://secretText-my.sharepoint.com/personal/blabla"))
{
if (ctx != null)
{
ctx.Load(ctx.Web); // Query for Web
ctx.ExecuteQuery(); // Execute
ctx.Load(ctx.Site);
ctx.ExecuteQuery();
SPSite site = new SPSite(ctx.Site.Id);
SPContentDatabase db = site.ContentDatabase;
// Get the first batch of changes,
SPChangeCollection changes = db.GetChanges();
//USE this 'site' object to fetch the change logs
.
.
.
My aim is to somehow instantiate this SPSite object which would then help me get the data I want. Although this code seems a bit too ambitious(or totally wrong) but please don't hold it against me, I couldn't find any solution to this.
Much appreciated!
After a lot of Google searches and after reading so many answers, I have come to know that it isn't possible to connect to a remote Sharepoint server through the Server API. As that API works only when SP server is on the same network (same machine or intranet)
The only solution is to use Client Object Model. It provides(maps) quite a lot operations that the Server API gives.
To connect to the remote site I have used the samples provided at the MSDN site for Client Object Model. Here
I currently have a couple of concurrency issues with the Task-based asynchronous API in the Azure SDK for .Net version 3.0.2-prerelease.
I have a list of web site names
var webSites = new [] { "website1", "website2" };
and from these, I'm using the task based API to create or delete the WebSites. Both occasionally fail:
await Task.WhenAll(webSites.Select(x => webSiteClient.WebSites.CreateAsync(
"westeuropewebspace",
new WebSiteCreateParameters
{
SiteMode = WebSiteMode.Limited,
ComputeMode = WebSiteComputeMode.Shared,
Name = x
WebSpaceName = "something"
}
)));
Seldom, I get an exception complaining that the Server Farm "Default1" already exists. I get that this server farm is implicitly created for Free web sites, but there is currently no way to create this Server Farm through the API before creating the WebSites (only the "DefaultServerFarm" can be).
When deleting, something similar happens:
await Task.WhenAll(webSites.Select(x => webSiteClient.WebSites.DeleteAsync(
"westeuropewebspace",
x,
new WebSiteDeleteParameters
{
DeleteAllSlots = true,
DeleteEmptyServerFarm = true,
DeleteMetrics = true,
}
)));
Often (about every second time), I get an Exception that "website2" could not be found, although it definitely existed. The WebSite is deleted, though.
Update:
I have serialized this second Task.WaitAll into a foreach-loop and I still get the exception. The only difference now is that when deleting "website1" fails, "website2" still exists in the cloud (because the second delete request is not sent) and I have to delete it manually through the portal.
You are right - the create site api also tries to create a server farm implicitly and if called concurrently that may cause conflicts. A safer way is to create a server farm explicitly using API and then use that server farm when creating web sites. That way you explicitly control the placement of sites to server farms and there are no implicit server farm creations.
The Azure SDK API contain a method to create server farm explicitly.
https://github.com/Azure/azure-sdk-for-net/blob/master/src/WebSiteManagement/Generated/ServerFarmOperations.cs
Is it possible to execute an SPDataSource object query in a console app for testing?
e.g.:
SPDataSource source = new SPDataSource
{
UseInternalName = true,
DataSourceMode = SPDataSourceMode.List,
SelectCommand = "<View/>"
};
source.SelectParameters.Add("WebId", TypeCode.String, "rootweb");
source.SelectParameters.Add("ListName", TypeCode.String, "Contacts");
var c = source.GetView();
var d = c.Select();
I think the context info is missing but can't figure out how to add it?
I just looked at it in Refelector and it ends up creating a class called SPDataSourceView which depends on SPContext.
I have never been able to creat an SPContext from a console application because of constructors marked as internal.
One option would be to put your class into a Web Service that is deployed to your SharePoint Farm. Then have your console application call this Web Service. However you might be better off using one of the Out of Box SharePoint Web Services.
I´m not sure what you´re after here, I mean
Testing your SPDataSource in console app (nothing to do like said by JD)
Getting data from sharepoint in a datasource manner.
If your´re going for solution 2 you could use a linqdatasource instead of the spdatasource.
See my post on this if that´s what your´re looking for.