SharePointOnline CSOM 401 Unauthorized Using Provided Access Token - c#

I am building a feature that automates the retrieval of documents and other SharePoint files from a Web API, but I'm having a difficult time getting authorized to perform even basic read operations. I am testing this in a .NET 6 console application using the Microsoft.SharePointOnline.CSOM NuGet package.
I have registered an application in Azure Active Directory and given it the Sites.Read.All permission. I've taken the ClientID, ClientSecret and TenantID as reported by that registered application and I'm using those in my console application. I can retrieve an access token without issue, and decoding that JWT shows that it comes with Sites.Read.All permission. But regardless of what I try, ClientContext.ExecuteQueryAsync() consistently throws an exception complaining that the remote server responded with a 401.
Here is the code that I'm testing this with:
var clientId = "myClientId";
var clientSecret = "myClientSecret";
var tenantId = "myTenantId";
var authority = "https://login.microsoftonline.com/" + tenantId;
var siteUrl = "https://myorg.sharepoint.com";
var app = new ConfidentialClientApplicationBuilder
.Create()
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.WithTenantId(tenantId)
.Build();
var paramBuilder = app.AcquireTokenForClient(new[] { siteUrl + "/.default" });
var authResult = await paramBuilder.ExecuteAsync();
// authResult has successfully retrieved an access token at this point
var context = new ClientContext(siteUrl);
context.ExecutingWebRequest += (_, e) =>
{
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + authResult.AccessToken;
}
context.Load(context.Web);
await context.ExecuteQueryAsync(); // 401 is thrown here
var title = context.Web.Title;
I have tried several different ways of getting around this to no avail:
I have gone to the Admin center of my SharePoint site and given the app FullControl permissions, as well as giving the app those permissions in Azure AD. This doesn't seem to have changed anything, I still get the same 401.
I have registered an entirely new app directly from my SharePoint sub-site admin center and given it FullControl permissions. I used the new client ID and client secret that were generated, and I was able to get back an access token. No luck, still get the 401 calling ClientContext.ExecuteQueryAsync()
I have tried changing my siteUrl to a SharePoint site-specific URL (e.g. https://myorg.sharepoint.com/sites/mySite), but once I do that I am no longer able to retrieve an access token. I instead get an Msal exception thrown, AADSTS500011, which reads:
"The resource principal named https://myorg.sharepoint.com/sites/mysite was not found in the tenant named (my tenant). This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.
I have also tried using the base siteUrl to retrieve the token, then giving the site-specific URL to ClientContext. I get the same 401 result.
I have tried several different authorities in case the token I'm being provided is invalid. I've tried using the V1 token URL, the V2 token URL, no token-specific URL (only the default authority address + tenant ID). All of these return an access token, but none of them avoid the 401.
A MS documentation article suggests appending an additional "/" to the requested .default scope in instances where a 401 is being returned (e.g. https://myorg.sharepoint.com/sites/mysite//.default). This doesn't seem to have changed anything.
My application seems to have the permissions it needs to do this basic read operation, but I am continually rebuffed. I am using the ClientID, ClientSecret and Tenant ID as copied directly from the AAD application page. The code I'm using above is recommended by Microsoft to use the new SharePointOnline.CSOM package. What am I missing here?

Constructor of ClientContext requires site url including site name.
var clientId = "myClientId";
var clientSecret = "myClientSecret";
var tenantId = "myTenantId";
var authority = "https://login.microsoftonline.com/" + tenantId;
var siteUrl = "https://myorg.sharepoint.com";
var siteName = "MySiteName";
var app = new ConfidentialClientApplicationBuilder
.Create()
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.WithTenantId(tenantId)
.Build();
var paramBuilder = app.AcquireTokenForClient(new[] { siteUrl + "/.default" });
var authResult = await paramBuilder.ExecuteAsync();
// authResult has successfully retrieved an access token at this point
var webFullUrl = $"{siteUrl}/sites/{siteName}";
var context = new ClientContext(webFullUrl);
If the site has some prefix
var webFullUrl = $"{siteUrl}/sites/{sitePrefix}/{siteName}";

I wound up "solving" this problem by using the PnP.Framework NuGet package instead of Microsoft.SharePointOnline.CSOM. I changed nothing else about my app registration or its designated permissions, and PnP.Framework was able to handle it without issue (and with fewer arguments). It seems to know something that SharePointOnline.CSOM doesn't considering that the following simple console app works:
using System;
using PnP.Framework
const string clientId = "myClientId";
const string clientSecret = "myClientSecret";
const string siteUrl = "https://myorg.sharepoint.com/sites/mysite";
using var clientContext = new AuthenticationManager()
.GetACSAppOnlyContext(siteUrl, clientId, clientSecret);
cc.Load(cc.Web);
await cc.ExecuteQueryAsync(); // no longer throws a 401
Console.WriteLine(cc.Web.Title); // prints my site's title
I tried to use the newer PnP.Core SDK, but I couldn't find any documentation or examples on how to get that package working with an app-only client secret authenticated context. PnP.Framework's API is the cleanest and most reliable that I've found as of yet.

Related

how reproduce result of powershell > az login in c#?

The result of "az login" is list of subscriptions: like here:
https://learn.microsoft.com/en-us/rest/api/resources/subscriptions/list?source=docs
How to make token for this request without providing tenantId or clientId exactly how it was made on website's login?
I can make token is quite close to required but do not have what I see inside token from website:
var functionCred = new Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential(clientId, secret);
var context = new AuthenticationContext("https://login.microsoftonline.com/" + tenantId, false);
var result = context.AcquireTokenAsync("https://management.core.windows.net/", functionCred).Result;
var token44 = result.AccessToken;
What should I do to improve token?
By default, az login command logs in with a user account. CLI will try to launch a web browser to log in interactively, if browser is not available then CLI will fall back to device code login.
For the question -
How to make token for this request without providing tenantId or clientId exactly how it was made on website's login?
It is not feasible to get an authentication token without using tenantId and clientId. Mostly token is generated by client id and secret. Token can be acquired via app registration + tenentId or user credentials + tenentId.
Below code snippet shows how you get the access token using Azure Active Directory Authentication Library (ADAL).
static AuthenticationResult AccessToken()
{
string resourceUri = "https://datacatalog.azure.com";
// register a client app and get a Client ID
string clientId = clientIDFromAzureAppRegistration;
//A redirect uri gives AAD more details about the specific application that it will authenticate.
string redirectUri = "https://login.live.com/oauth20_desktop.srf";
// Create an instance of AuthenticationContext to acquire an Azure access token
string authorityUri = "https://login.windows.net/common/oauth2/authorize";
AuthenticationContext authContext = new AuthenticationContext(authorityUri);
// AcquireToken takes a Client Id that Azure AD creates when you register your client app.
return authContext.AcquireToken(resourceUri, clientId, new Uri(redirectUri), PromptBehavior.RefreshSession);
}
For more information check this Steps to get an access token section of the Microsoft Document.

How to fix issue calling Amazon SP-API, which always returns Unauthorized, even with valid Token and Signature

I went through the guide of for getting setup to call the new SP-API (https://github.com/amzn/selling-partner-api-docs/blob/main/guides/developer-guide/SellingPartnerApiDeveloperGuide.md), and during the process checked off all of the api areas to grant access to (i.e. Orders, Inventory, etc). I am using the C# library provided by Amazon (https://github.com/amzn/selling-partner-api-models/tree/main/clients/sellingpartner-api-aa-csharp). I successfully get an access token and successfully sign the request, but always get the following error:
Access to requested resource is denied. / Unauthorized, with no details.
I am trying to perform a simple get to the /orders/v0/orders endpoint. What am I doing wrong?
Below is my code:
private const string MARKETPLACE_ID = "ATVPDKIKX0DER";
var resource = $"/orders/v0/orders";
var client = new RestClient("https://sellingpartnerapi-na.amazon.com");
IRestRequest restRequest = new RestRequest(resource, Method.GET);
restRequest.AddParameter("MarketPlaceIds", MARKETPLACE_ID, ParameterType.QueryString);
restRequest.AddParameter("CreatedAfter", DateTime.UtcNow.AddDays(-5), ParameterType.QueryString);
var lwaAuthorizationCredentials = new LWAAuthorizationCredentials
{
ClientId = AMAZON_LWA_CLIENT_ID,
ClientSecret = AMAZON_LWA_CLIENT_SECRET,
RefreshToken = AMAZON_LWA_REFRESH_TOKEN,
Endpoint = new Uri("https://api.amazon.com/auth/o2/token")
};
restRequest = new LWAAuthorizationSigner(lwaAuthorizationCredentials).Sign(restRequest);
var awsAuthenticationCredentials = new AWSAuthenticationCredentials
{
AccessKeyId = AMAZON_ACCESS_KEY_ID,
SecretKey = AMAZON_ACCESS_SECRET,
Region = "us-east-1"
};
restRequest = new AWSSigV4Signer(awsAuthenticationCredentials).Sign(restRequest, client.BaseUrl.Host);
var response = client.Execute(restRequest);
If you followed the SP-API guide, then you created a Role (which is the IAM ARN your app is registered with) and a User which has permissions to assume that role to make API calls.
However, one thing the guide is not clear about is that you can't make API calls using that user's credentials directly. You must first call the STS API's AssumeRole method with your User's credentials (AMAZON_ACCESS_KEY_ID/AMAZON_ACCESS_SECRET), and it will return temporary credentials authorized against the Role. You use those temporary credentials when signing requests.
AssumeRole will also return a session token which you must include with your API calls in a header called X-Amz-Security-Token. For a brief description of X-Amz-Security-Token see https://docs.aws.amazon.com/STS/latest/APIReference/CommonParameters.html
You also get this error if your sp app is under review, drove me nuts!
If you using c# take look to
https://github.com/abuzuhri/Amazon-SP-API-CSharp
AmazonConnection amazonConnection = new AmazonConnection(new AmazonCredential()
{
AccessKey = "AKIAXXXXXXXXXXXXXXX",
SecretKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
RoleArn = "arn:aws:iam::XXXXXXXXXXXXX:role/XXXXXXXXXXXX",
ClientId = "amzn1.application-XXX-client.XXXXXXXXXXXXXXXXXXXXXXXXXXXX",
ClientSecret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
RefreshToken= "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
});
var orders= amazonConnection.Orders.ListOrders();
In our situation, we had to explicitly add an IAM policy to the user we defined as making the API call. Please see the link below and confirm that the user you have calling the API has the policy assigned to them:
https://github.com/amzn/selling-partner-api-docs/blob/main/guides/developer-guide/SellingPartnerApiDeveloperGuide.md#step-3-create-an-iam-policy
Somehow we went through the step-by-step setup twice, and adding this explicit policy was missed. Initially I believe it was added 'inline' as instructed, but that does not seem to work.
I dont think is a duplicated question, buy the solution may apply: https://stackoverflow.com/a/66860192/1034622

Authentication to Azure Active Directory and receive AADSTS90019 error

I am trying to authenticate against AAD using the following code:
string userName = "something.com"; //(just an example)
string password = "IafksdfkasdaFadad=asdad=a="; //(just an example)
string clientId = "6cd6590f-4db9-4c6b-98d1-476f9e90912f"; //(just an example)
var credentials = new UserPasswordCredential(userName, password);
var authenticationContext = new AuthenticationContext("https://login.windows.net/common");
var result = await authenticationContext.AcquireTokenAsync("https://api.partnercenter.microsoft.com", clientId, credentials);
return result;
and I got AADSTS90019 error: No tenant-identifying information found in either the request or implied by any provided credentials.
As a remark, it is just a console application made in Visual Studio using C#.
Based on the information from https://learn.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes the explication for AADSTS90019 error is: MissingTenantRealm - Azure AD was unable to determine the tenant identifier from the request.
So, my question is: What is the tentant identifier and how should I use it in my request?
Should it be the one from the following screenshot? The screenshot is made from the Azure account.
Azure Application Overview
Any information can help.
Thank you.
You should initialize your authentication context with a tenant-specific authority instead of common:
var authenticationContext = new AuthenticationContext("https://login.microsoftonline.com/your-directory-id");
Replace your-directory-id with your Directory (tenant) id.
var authenticationContext = new AuthenticationContext("https://login.windows.net/common");
Here Replace the string "common" with the tenant name.

Microsoft Graph The token contains no permissions, or permissions cannot be understood

I am working with Microsoft Graph and have created an app that reads mail from a specific user.
However, after getting an access token and trying to read the mailfolders, I receive a 401 Unauthorized answer. The detail message is:
The token contains no permissions, or permissions cannot be understood.
This seems a pretty clear message, but unfortunately I am unable to find a solution.
This is what I have done so far:
Registering the app on https://apps.dev.microsoft.com
Giving it
application permissions Mail.Read, Mail.ReadWrite
(https://learn.microsoft.com/en-us/graph/api/user-list-mailfolders?view=graph-rest-1.0)
Have gotten administrator consent.
The permissions are:
- Written the code below to acquire an access token:
// client_secret retrieved from secure storage (e.g. Key Vault)
string tenant_id = "xxxx.onmicrosoft.com";
ConfidentialClientApplication client = new ConfidentialClientApplication(
"..",
$"https://login.microsoftonline.com/{tenant_id}/",
"https://dummy.example.com", // Not used, can be any valid URI
new ClientCredential(".."),
null, // Not used for pure client credentials
new TokenCache());
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = client.AcquireTokenForClientAsync(scopes).Result;
string token = result.AccessToken;
So far so good. I do get a token.
Now I want to read the mail folders:
url = "https://graph.microsoft.com/v1.0/users/{username}/mailFolders";
handler = (HttpWebRequest)WebRequest.Create(url);
handler.Method = "GET";
handler.ContentType = "application/json";
handler.Headers.Add("Authorization", "Bearer " + token);
response = (HttpWebResponse)handler.GetResponse();
using (StreamReader sr = new StreamReader(response.GetResponseStream()))
{
returnValue = sr.ReadToEnd();
}
This time I receive a 401 message, with the details:
The token contains no permissions, or permissions cannot be understood.
I have searched the internet, but can’t find an answer to why my token has no permissions.
Thanks for your time!
update 1
If I use Graph Explorer to read the mailfolders, then it works fine. Furthermore: if I grap the token id from my browser en use it in my second piece of code, then I get a result as well. So, the problem is really the token I receive from the first step.
To ensure this works like you expect, you should explicitly state for which tenant you wish to obtain the access token. (In this tenant, the application should, of course, have already obtained admin consent.)
Instead of the "common" token endpoint, use a tenant-specific endpoint:
string url = "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token";
(Where {tenant-id} is either the tenant ID of the tenant (a Guid), or any verified domain name.)
I would also strongly recommend against building the token request on your own, as you show in your question. This may be useful for educational purposes, but will tend to be insecure and error-prone in the long run.
There are various libraries you can use for this instead. Below, an example using the Microsoft Authentication Library (MSAL) for .NET:
// client_secret retrieved from secure storage (e.g. Key Vault)
string tenant_id = "contoso.onmicrosoft.com";
ConfidentialClientApplication client = new ConfidentialClientApplication(
client_id,
$"https://login.microsoftonline.com/{tenant_id}/",
"https://dummy.example.com", // Not used, can be any valid URI
new ClientCredential(client_secret),
null, // Not used for pure client credentials
new TokenCache());
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
AuthenticationResult result = client.AcquireTokenForClientAsync(scopes).Result
string token = result.AccessToken;
// ... use token

Azure AD authentication with asp.net Identity for authorisation

I tried to look for all over internet but couldn't see how I can achieve what I was asked to. Here is my enterprise app which uses Asp.net Identity for form based authentication. I had extended User and Role along with Groups to provide authorization in my code. (note: not using any group/role directives).
Now I was asked to look at possibility of changing code to accommodate Azure Active Directory authentication. I tried reading on how you can register app, send user to Azure site for authentication, get back token etc. However I'm stuck at 'what-afterwards?' I have authenticated user How can I use my existing Asp.net Identity model where user was stored in sql database. How to use this token to relate the existing user.
Moreover, when I change my project to allow Azure AD, it removes Aspnet.Identity package as its not compatible with Azure AD !!
I even tried manually keeping both packages side by side, I got to point where user is sent to authenticate on Azure, diverted back to home page and again to login on Azure AD in never ending loop.
to summarize the question, How can I authenticate user from AAD and keep using existing Roles and groups authorization.
Edit:
I tried creating separate web service which will authenticate user and send JWT token. which works find if I call it directly on browser, however, when I tried to call this service from my web app I get weird error
Application with identifier 'a2d2---------------' was not found in the directory azurewebsites.net
Weird part here is name of directory is 'azurewebsites.net' and not the default directory I'm using.
Update
Here is code which throws error
public async Task<ActionResult> Login(string returnUrl)
{
try
{
// get the access token
AuthenticationContext authContext = new AuthenticationContext(authority, new TokenCache());
var clientCredential = new ClientCredential(clientId, password);
//Error on below line
AuthenticationResult result = await authContext.AcquireTokenAsync(resourceId, clientCredential);
// give it to the server to get a JWT
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
......
try this:
var client = new RestClient("https://login.microsoftonline.com/{tenant-
Id}/oauth2/v2.0/token");
var request = new RestRequest(Method.POST);
request.AddHeader("cache-control", "no-cache");
request.AddHeader("content-type", "application/x-www-form-urlencoded");
request.AddHeader("grant_type", "password");
request.AddParameter("application/x-www-form-urlencoded",
"grant_type=password&client_id={client-Id}&client_secret={client-
secret}&scope={scopeurl}&userName={username}&password={password}",
ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
var json = response.Content;
var JSONObject = JObject.Parse(json);
var token = (string)JSONObject["access_token"];
I had a similar issue so I created an Office 365 owin security plugin. I shared the code on github. It's based on the katana project at codeplex.
You can find the source code at https://github.com/chadwjames/wakizashi.
You will need to register your application here. When registering the application set the call back uri to https://yourdomain/signin-office365
The Application ID is your Client Id and the Password is your Client Secret.
Once you have it registered you can modify the Startup.Auth.cs and add something like this to the ConfigureAuth method.
//setup office 365
var office365Options = new Office365AuthenticationOptions
{
ClientId = ConfigurationManager.AppSettings["ada:ClientId"],
ClientSecret = ConfigurationManager.AppSettings["ada:ClientSecret"],
Provider = new Office365AuthenticationProvider
{
OnAuthenticated = async context =>
{
await
Task.Run(
() => context.Identity.AddClaim(new Claim("Office365AccessToken", context.AccessToken)));
}
},
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie
};
office365Options.Scope.Add("offline_access");
app.UseOffice365Authentication(office365Options);
When I have more time I hope to create a nuget package for this.

Categories