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
Related
My ASP.Net Core MVC app accesses a .Net Core API through IdentityServer. It works fine on IIS server running in-process with Entity Framework based identity store. Now I am trying to enable Windows Authentication and getting stuck here.
What I tried is following the identityserver doc section "Windows Authentication" - I added the code below to the ConfigureServices of my IdentityServer's Startup.cs
// configures IIS in-proc settings
services.Configure<IISServerOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
I also enabled the Windows Authentication in IIS for my API app
The part of the doc that I am confused about is "You trigger Windows authentication by calling ChallengeAsync on the Windows scheme". It doesn't mention where you do that. I am assuming it is in identityserver and I put the code in the Login method of the AccountController of the identityserver as bellow.
/// <summary>
/// Entry point into the login workflow
/// </summary>
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
// trigger Windows authentication by calling ChallengeAsync
await ChallengeWindowsAsync(returnUrl);
// build a model so we know what to show on the login page
var vm = await BuildLoginViewModelAsync(returnUrl);
if (vm.IsExternalLoginOnly)
{
// we only have one option for logging in and it's an external provider
return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl });
}
return View(vm);
}
private async Task<IActionResult> ChallengeWindowsAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync("Windows");
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, treating windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", "Windows" },
}
};
var id = new ClaimsIdentity("Windows");
// the sid is a good sub value
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.FindFirst(ClaimTypes.PrimarySid).Value));
// the account name is the closest we have to a display name
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
// add the groups as claims -- be careful if the number of groups is too large
var wi = wp.Identity as WindowsIdentity;
// translate group SIDs to display names
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
await HttpContext.SignInAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props);
return Redirect(props.RedirectUri);
}
else
{
// trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge("Windows");
}
}
What I expect to happen, if everything goes well, is that the authentication happens automatically (without a login box?) because the "Challenge" call will require the client side (the browser) to send in Windows identity info and a token will be issued based on that.
It doesn't seem to work that way now - I am getting an Unauthorized error from API when starting the MVC app:
Am I doing that in the wrong place? Or am I missing something else?
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;
}
}
I created a basic project using Visual Studio 2015 Update 3 for Web API (nothing custom, bare bone) and deployed it to Azure (Free Account) following the instruction here.
Then I created a Console client with the following code.
public static async Task<bool> ReadValues()
{
try
{
// Authenticate the user and get a token from Azure AD
//AuthenticationResult authResult = await AuthContext.AcquireTokenSilentAsync(Resource, ClientId);
AuthenticationResult authResult = AuthContext.AcquireToken(Resource, ClientId, RedirectUri);
// Create an HTTP client and add the token to the Authorization header
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
//"Bearer"
authResult.AccessTokenType
, authResult.AccessToken);
// Call the Web API to get the values
var requestUri = new Uri(WebApiUri, "api/values");
Console.WriteLine("Reading values from '{0}'.", requestUri);
HttpResponseMessage httpResponse = await httpClient.GetAsync(requestUri);
Console.WriteLine("HTTP Status Code: '{0}'", httpResponse.StatusCode.ToString());
//Console.WriteLine("HTTP Header: '{0}'", httpClient.DefaultRequestHeaders.Authorization.ToString());
if (httpResponse.IsSuccessStatusCode)
{
//
// Code to do something with the data returned goes here.
//
var s = await httpResponse.Content.ReadAsStringAsync();
Console.WriteLine(s);
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(httpResponse.ReasonPhrase);
}
return (httpResponse.IsSuccessStatusCode);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return false;
}
It works fine when I run the WEB API locally from the Visual Studio in debug, but when I deploy it to the Azure, it returns Unauthorized.
Few common things that I might get asked:
I do receive a valid bearer token
I have created the App registrations in the Azure AD for bot hthe WEB API and the client
The client and WEB API are using the correct redirect, resource uri
The account I am using to login is the same as the one used to create the Azure account and it has full privileges in the domain/AD/API
On the API side, this is whole of the startup.auth.cs
using System.Configuration;
using System.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security.ActiveDirectory;
using Owin;
using WebApi;
[assembly: OwinStartup("default", typeof(Startup))]
namespace WebApi
{
public partial class Startup
{
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters {
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
},
});
}
}
}
What else should I check?
Other references
https://www.simple-talk.com/cloud/security-and-compliance/azure-active-directory-part-3-developing-native-client-applications/
Thanks for help from Juunas who provided me with a working copy, I was able to narrow down the cause. When I attached a debugger to the Azure instance of the Web API I was able to see a exception for Bad Audience. On trying to retrace my steps, I found that while deployment from Visual Studio, I was selection Enterprise Authentication in settings that was causing the web.config to change in way that lead to the problem. Not selecting that option, I was able to access the API through bearer token.
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.
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.