SharePoint 2010 Client Object Model - Kerberos/Claims Authentication - sharepoint

I'm trying to read a value from a list in a remote SharePoint site (different SP Web App). The web apps are set up with Claims Auth, and the client web app SP Managed account is configured with an SPN. I believe Kerberos and claims are set up correctly, but I am unable to reach the remote server, and the request causes an exception: "The remote server returned an error: (401) Unauthorized."
The exception occurs in the line ctx.ExecuteQuery(); but it does not catch the exception in the if (scope.HasException) instead, the exception is caught by the calling code (outside of the using{} block).
When I look at the traffic at the remote server using Wireshark, it doesn't look like the request is even getting to the server; it's almost as if the 401 occurs before the Kerberos ticket is exchanged for the claim.
Here's my code:
using (ClientContext ctx = new ClientContext(contextUrl))
{
CredentialCache cc = new CredentialCache();
cc.Add(new Uri(contextUrl), "Kerberos", CredentialCache.DefaultNetworkCredentials);
ctx.Credentials = cc;
ctx.AuthenticationMode = ClientAuthenticationMode.Default;
ExceptionHandlingScope scope = new ExceptionHandlingScope(ctx);
Web ctxWeb = ctx.Web;
List ctxList;
Microsoft.SharePoint.Client.ListItemCollection listItems;
using (scope.StartScope())
{
using (scope.StartTry())
{
ctxList = ctxWeb.Lists.GetByTitle("Reusable Content");
CamlQuery qry = new CamlQuery();
qry.ViewXml = string.Format(ViewQueryByField, "Title", "Text", SharedContentTitle);
listItems = ctxList.GetItems(qry);
ctx.Load(listItems, items => items.Include(
item => item["Title"],
item => item["ReusableHtml"],
item => item["ReusableText"]));
}
using (scope.StartCatch()) { }
using (scope.StartFinally()) { }
}
ctx.ExecuteQuery();
if (scope.HasException)
{
result = string.Format("Error retrieving content<!-- Error Message: {0} | {1} -->", scope.ErrorMessage, contextUrl);
}
if (listItems.Count == 1)
{
Microsoft.SharePoint.Client.ListItem contentItem = listItems[0];
if (SelectedType == SharedContentType.Html)
{
result = contentItem["ReusableHtml"].ToString();
}
else if (SelectedType == SharedContentType.Text)
{
result = contentItem["ReusableText"].ToString();
}
}
}
I realize the part with the CredentialCache shouldn't be necessary in claims, but every single example I can find is either running in a console app, or in a client side application of some kind; this code is running in the codebehind of a regular ASP.NET UserControl.
Edit: I should probably mention, the code above doesn't even work when the remote URL is the root site collection on the same web app as the calling code (which is in a site collection under /sites/)--in other words, even when the hostname is the same as the calling code.
Any suggestions of what to try next are greatly appreciated!
Mike

Is there a reason why you are not using the standard OM?
You already said this is running in a web part, which means it is in the context of application pool account. Unless you elevate permissions by switching users, it won't authenticate correctly. Maybe try that. But I would not use the client OM when you do have access to the API already.

Related

Querying On-premise SharePoint using Azure AD MFA through C# app

I'm trying to use Microsoft.Identity.Client and Microsoft.SharePoint.Client libraries to authenticate to an On-premise SharePoint server and then query it.
I obtain the Azure AD access token from which the SharePoint server is a part of like following:
private readonly string[] m_scopes = { "user.read", "https://sql.azuresynapse-dogfood.net/user_impersonation" };
var publicAppBuilder = PublicClientApplicationBuilder.Create("MyClientId").WithAuthority("https://login.microsoftonline.com/a******com.onmicrosoft.com");
publicAppBuilder.WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient");
var app = publicAppBuilder.Build();
AuthenticationResult result = null;
result = app.AcquireTokenInteractive(m_scopes).ExecuteAsync().GetAwaiter().GetResult();
if (result != null)
{
m_mediator.AccessToken = result.AccessToken;
}
When I get the access token I put it in the request header as follows:
args.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + m_mediator.AccessToken;
Which is inside the ClientContext.ExecutingWebRequest subscribed method:
clientContext.ExecutingWebRequest += (sender, args) =>
which is triggered by
context.ExecuteQuery();
The remote server returned an error: (401) Unauthorized.
or
The remote server returned an error: (403) Forbidden.
How can I establish the connection? I want to avoid using app-only registration, I want to authenticate using Azure AD MFA (Interactive) method.Please note that I have all the permissions needed and I am an admin on both Azure AD where SharePoint is joined, as well on the SharePoint server itself. I authenticate through the browser just fine.
I've tried multiple things so far:
I tried creating a separate request where I forward the previously acquired accessToken as Authorization: Bearer token
I tried reading the FedAuth from the authentication connection window, so I can forward it in my HTTP request but with no success
I tried creating a "Web browser" using a WebBrowser C# class and reading the cookies that are on a browser level like the following: cookieContainer = webBrowser1.Document.Cookie; but I had no success.
I'm expecting to Authenticate via Azure AD and then connect to SharePoint in order to query it
To resolve the error "The remote server returned an error: (401)
Unauthorized", please try checking the following:
Check whether your URL is correct:
The SharePoint Online URL must always start with HTTPS.
$SiteURL` `=` `"https://crescent.sharepoint.com/sites/marketing"`
Check if you have the right permissions to the site:
Check whether you have sufficient permissions and you are able to open the site in the browser. Make sure to have SharePoint Online Administrator Role.
Check whether the Legacy authentication protocol is enabled:
Make sure to enable Legacy authentication protocol in your tenant, if it is not enabled.
Reference : SharePoint Online: Fix "The remote server returned an error (401) Unauthorized" Error in PowerShell - SharePoint Diary
To resolve the error "The remote server returned an error: (403)
Forbidden.", please try checking the following:
Make sure whether you have provided correct URL and credentials.
Make sure whether you have installed latest version of SharePoint Online Client Component SDK.
Try adding yourself to the site explicitly
Check the lock status of your site and unlock if it is locked.
Please check if any conditional access policies is enabled in your tenant.
If you try to connect to the Tenant Admin site, make sure the Tenant Admin URL like below:
https://YourDomain-admin.sharepoint.com
Reference : SharePoint Online: Fix "The remote server returned an error: (403) Forbidden." Error in PowerShell - SharePoint Diary.
I've found a solution.
I basically iterate through all cookies whenever a browser navigates through a new page and parse all the cookies until I get the fedAuth cookie:
I created a web browser from System.Windows.Forms.WebBrowser
In the WebBrowserNavigatedEventHandler for Navigated I do the following:
if (webBrowser1.Url.AbsoluteUri == "about:blank")
{
return;
}
var cookieData = GetWebBrowserCookie.GetCookieInternal(webBrowser1.Url, false);
if (string.IsNullOrEmpty(cookieData) == false)
{
var dict = ParseCookieData(cookieData);
if (dict.ContainsKey("FedAuth") && !string.IsNullOrEmpty(dict["FedAuth"]))
{
m_mediator.FedAuthCookie = dict["FedAuth"];
if (dict.ContainsKey("rtFa") && !string.IsNullOrEmpty(dict["rtFa"]))
{
m_mediator.RtFaCookie = dict["rtFa"];
}
m_mediator.UpdateConfiguration();
this.Close();
}
}
The ParseCookieData method looks like this:
private IDictionary<string, string> ParseCookieData(string cookieData)
{
var cookieDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(cookieData))
{
return cookieDictionary;
}
var values = cookieData.TrimEnd(';').Split(';');
foreach (var parts in values.Select(c => c.Split(new[] { '=' }, 2)))
{
var cookieName = parts[0].Trim();
var cookieValue = parts.Length == 1 ? string.Empty : parts[1];
cookieDictionary[cookieName] = cookieValue;
}
return cookieDictionary;
}
and GetWebBrowserCookie class looks like this:
[SecurityCritical]
public static string GetCookieInternal(Uri uri, bool throwIfNoCookie)
{
uint pchCookieData = 0;
string url = UriToString(uri);
uint flag = (uint)NativeMethods.InternetFlags.INTERNET_COOKIE_HTTPONLY;
//Gets the size of the string builder
if (NativeMethods.InternetGetCookieEx(url, null, null, ref pchCookieData, flag, IntPtr.Zero))
{
pchCookieData++;
StringBuilder cookieData = new StringBuilder((int)pchCookieData);
//Read the cookie
if (NativeMethods.InternetGetCookieEx(url, null, cookieData, ref pchCookieData, flag, IntPtr.Zero))
{
DemandWebPermission(uri);
return cookieData.ToString();
}
}
int lastErrorCode = Marshal.GetLastWin32Error();
if (throwIfNoCookie || (lastErrorCode != (int)NativeMethods.ErrorFlags.ERROR_NO_MORE_ITEMS))
{
throw new Win32Exception(lastErrorCode);
}
return null;
}
private static void DemandWebPermission(Uri uri)
{
string uriString = UriToString(uri);
if (uri.IsFile)
{
string localPath = uri.LocalPath;
new FileIOPermission(FileIOPermissionAccess.Read, localPath).Demand();
}
else
{
new WebPermission(NetworkAccess.Connect, uriString).Demand();
}
}
private static string UriToString(Uri uri)
{
if (uri == null)
{
return string.Empty;
}
UriComponents components = (uri.IsAbsoluteUri ? UriComponents.AbsoluteUri : UriComponents.SerializationInfoString);
return new StringBuilder(uri.GetComponents(components, UriFormat.SafeUnescaped), 2083).ToString();
}
This way we open up a pop-up C# web browser, authenticate the user through the web using MFA and then close the browser when we acquire an authentication cookie so we can continue working with HTTP requests towards the Sharepoint server.
Source: https://github.com/OceanAirdrop/SharePointOnlineGetFedAuthAndRtfaCookie

C# CSOM Sharepoint Bearer request from azure active directory

I am using the following approach as the basis of this (https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-devquickstarts-webapi-dotnet).
I got all this example working after setting up azure. But now we need to port it to an actual existing mobile app and web api app. The mobile app can get the Bearer token, but when we pass it to the web api, we pass this in a CSOM request as follows, but we still get a 401 Unauthroised response.
public static ClientContext GetSharepointBearerClientContext(this JwtTokenDetails tokenDetails)
{
var context = new ClientContext(tokenDetails.SiteUrl);
//context.AuthenticationMode = ClientAuthenticationMode.Anonymous;
context.ExecutingWebRequest += new EventHandler<WebRequestEventArgs>((s, e) =>
{
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + tokenDetails.BearerToken;
});
return context;
}
Our web api doesn't use any of the tech as in the example above, as I presume that we should just be able to pass the token through the CSOM request in the header, but this is not working, what else could I look at?
I have assigned the Office 365 Sharepoint Online (Microsoft.Sharepoint) permission and set the following
I have also done the same for the app registration, which we don't really use! Still not sure how the app registration comes into it)...
So this was possible, it was just microsoft telling us to put in an incorrect value. All the documentation says put the APP ID URI in the Resource. But in our case it needed to be the sharepoint url.
So we have the tenant name which on azure id the domain name e.g. srmukdev.onmicrosoft.com
Tenant: srmukdev.onmicrosoft.com
Application Id: This is the guid for the app registered in azure active directory.
RedirectUri: This can be any url(URI), its not actually used as a url for a mobile app as far as I can see.
ResourceUrl: srmukdev.sharepoint.com
The code I am using to get a token is as follows for a WPF example. The aadInstance is https://login.microsoftonline.com/{0}
private static string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
public async void CheckForCachedToken(PromptBehavior propmptBehavior)
{
//
// As the application starts, try to get an access token without prompting the user. If one exists, populate the To Do list. If not, continue.
//
AuthenticationResult result = null;
try
{
result = await authContext.AcquireTokenAsync(resourceUrl, applicationId, redirectUri, new PlatformParameters(propmptBehavior));
TokenTextBox.Text = result.AccessToken;
// A valid token is in the cache - get the To Do list.
GetTokenButton.Content = "Clear Cache";
}
catch (AdalException ex)
{
if (ex.ErrorCode == "user_interaction_required")
{
// There are no tokens in the cache. Proceed without calling the To Do list service.
}
else
{
// An unexpected error occurred.
string message = ex.Message;
if (ex.InnerException != null)
{
message += "Inner Exception : " + ex.InnerException.Message;
}
MessageBox.Show(message);
}
return;
}
}

Access Denied/401 error when running clientContext.ExecuteQuery()

I'm creating an MVC app that requires me to nab the group users from a Sharepoint site. I'm getting a 401 error when I publish and run it via IIS (localhost). When I run it through the debugger though, everything works as intended.
I'm assuming it's a permissions error based on the context. Is there a way to run the app normally using my current NTLM credentials programmatically (instead of what seems like the IIS account/user)?
Code
(I'm on mobile right now, so I'm sorry if anything looks wonky...)
// Constructor
clientContext = new ClientContext("http://sharepointsite");
clientContext.Credentials = CredentialCache.DefaultNetworkCredentials;
//clientContext.Credentials = new NetworkCredentials("username", "password", "domain"); <- this works just fine if I hard code my current credentials
// Calling method
UserCollection users = GetUsersCollection();
clientContext.Load(users);
try {
clientContext.ExecuteQuery(); // 401 error here
return true;
}
catch {
return false;
}
You can specify the credentials you connect to SharePoint with CSOM it would look something like this:
clientContext.Credentials = new NetworkCredential("Username", "Password", "Domain");
You can do MVC impersonation like this:
string currentUser = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
System.Security.Principal.WindowsImpersonationContext impersonationContext;
impersonationContext =
((System.Security.Principal.WindowsIdentity)User.Identity).Impersonate();
currentUser += "Impersonate:" + System.Security.Principal.WindowsIdentity.GetCurrent().Name;
impersonationContext.Undo();
ViewBag.U = currentUser;
return View();
I found that example in this thread:
http://forums.asp.net/t/1961337.aspx?Asp+Net+MVC+5+how+to+impersonate+user+on+IIS

Sharepoint claims based vs classic authentication

I have 2 sharepoint sites running out of one sharepoint installation. One site has claims based enabled and the other has classic auth enabled. Both sites also both use Kerberos.
I am using ManifoldCF to connect to these sites to extract the all content as well as the permissions. The ManifoldCF connector connects to the site with classic auth enabled and works as expected. However, trying to crawl the claims based site generates a 401 unauthorised error.
There is a web service package supplied with ManifoldCF that is accessed called MCPermissions.asmx. This file contains the following block of code which sets the user's credentials:
try
{
// Only handle requests for "item". Send all other requests to the SharePoint web service.
if (objectType.Equals(itemType))
{
retVal = GetItemPermissions(objectName);
}
else
{
ServicePointManager.ServerCertificateValidationCallback +=
new RemoteCertificateValidationCallback(ValidateCertificate);
using (SPPermissionsService.Permissions service = new SPPermissionsService.Permissions())
{
service.Url = SPContext.Current.Web.Url + "/_vti_bin/Permissions.asmx";
service.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
retVal = service.GetPermissionCollection(objectName, objectType);
}
}
}
catch (SoapException soapEx)
{
throw soapEx;
}
catch (Exception ex)
{
SPDiagnosticsService.Local.WriteTrace(0, new SPDiagnosticsCategory("MCPermissions.asmx", TraceSeverity.Unexpected, EventSeverity.Error), TraceSeverity.Unexpected, "Error: "+ex.Message+"; SPContext.Current.Web.Url='"+SPContext.Current.Web.Url+"'", ex.StackTrace);
throw RaiseException(ex.Message, "1000", ex.Source);
}
This code is accessed via ManifoldCF correctly, but it seems its the request made in the plugin to /_vti_bin/Permissions.asmx that is causing the 401 issue.
I have tried setting pre defined credentials in the code above using NetworkCredential
("username", "password", "domain") but no luck.
Example:
string webUrl = SPContext.Current.Web.Url;
NetworkCredential myCredentials = new NetworkCredential("DOMAIN\\user", "mypassword", "DOMAIN");
CredentialCache credCache = new CredentialCache();
credCache.Add(new Uri(webUrl), "Negotiate", myCredentials);
service.Url = webUrl + "/_vti_bin/Permissions.asmx";
service.Credentials = credCache;
With the claims based authentication, the username that i enter into ManfoldCF or in the browser authentication gets changed from the regular \ format to the claims username format (e.g. i:0#.w||/).
Does anybody know why claims based would be causing the 401 issue where the classic authentication does not?
Turns out this was a bug with the ManifoldCF 1.2 release. It has been fixed in newer versions.

File uploading from web application to Sharepoint server

This is my first time ever with Sharepoint. Here is the scenario
I have a stand alone web application
I also have a stand alone sharepoint server.
Both are on different servers.
I need to upload a file from web application to sharepoint
I found 2 methods online,
Using the webservice provided by Sharepoint (CopyIntoItems)
Using jQuery library of Sharepoint webservice
After searching the web, I think the jQuery part will not work (you can correct me).
I am looking for a method that takes username/password and uploads a pdf file to Sharepoint server. The following is my C# code that tries to upload but ends up in error
public bool UploadFile(string file, string destination)
{
bool success = false;
CopySoapClient client = new CopySoapClient();
if (client.ClientCredentials != null)
client.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
try
{
client.Open();
string filename = Path.GetFileName(file);
string destinationUrl = destination + filename;
string[] destinationUrls = { destinationUrl };
FieldInformation i1 = new FieldInformation { DisplayName = "Title", InternalName = "Title", Type = FieldType.Text, Value = filename };
FieldInformation[] info = { i1 };
CopyResult[] result;
byte[] data = File.ReadAllBytes(file);
//uint ret = client.CopyIntoItems(filename, destinationUrls, info, data, out result);
uint ret = client.CopyIntoItems(file, destinationUrls, info, data, out result);
if (result != null && result.Length > 0 && result[0].ErrorCode == 0)
success = true;
}
finally
{
if (client.State == System.ServiceModel.CommunicationState.Faulted)
client.Abort();
if (client.State != System.ServiceModel.CommunicationState.Closed)
client.Close();
}
return success;
}
I am calling the above function like this
UploadFile(#"C:\temp\uploadFile.txt", "http://spf-03:300/demo/Dokumente").ToString();
Error that i get:
Error Code: Destination Invalid
Error Message: The service method 'Copy' must be called on the same domain that contains the target URL.
There is the 3rd option with SharePoint 2010 and that is to use the Client Side object model. The client side object model a a sub set of the larger Sharepoint API, but it does cover uploading documents. Below is blog post with an example of uploading.
Upload document through client object model
As with most things in SharePoint you will need to authenticate against it the site, so find out if your site collection is forms based or claims based and then you should be able to find sample code for your situation.
Solution to the problem:
The problem was that the "security token webservice" was not working and it was giving some error when we manually ran the webservice.
The server was unable to process the request due to an internal error.
For more information about the error, either turn on
IncludeExceptionDetailInFaults (either from ServiceBehaviorAttribute
or from the configuration behavior) on the server in order to send the
exception information back to the client, or turn on tracing as per
the Microsoft .NET Framework 3.0 SDK documentation and inspect the
server trace logs.
The above exception is a generic one. To view the exact exception we enabled remote error viewing from the web.config file of the webservice(link) and saw the exact exception.
We found the solution for the exception and the service started. After that everything was working fine.

Resources