After Microsoft deprecated basic authentication for EWS I am trying to update an existing app in order to support the newer type of authentication. However, whatever I do, the call ends with 403 Forbidden.
I'd be thankful for any kind of help.
Code
var cca = ConfidentialClientApplicationBuilder
.Create("AppId")
.WithClientSecret("ClientSecret")
.WithTenantId("TenantId")
.Build();
var ewsScopes = new string[] { "https://outlook.office365.com/.default" };
var emailToBeRead = "test#test.com";
try
{
var authResult = cca.AcquireTokenForClient(ewsScopes)
.ExecuteAsync().Result;
// Configure the ExchangeService with the access token
var ewsClient = new ExchangeService
{
Url = new Uri("https://outlook.office365.com/EWS/Exchange.asmx"),
Credentials = new OAuthCredentials(authResult.AccessToken),
ImpersonatedUserId =
new ImpersonatedUserId(ConnectingIdType.SmtpAddress, emailToBeRead)
};
//Include x-anchormailbox header
ewsClient.HttpHeaders.Add("X-AnchorMailbox", emailToBeRead);
// Make an EWS call
var folders = ewsClient.FindFolders(WellKnownFolderName.MsgFolderRoot, new FolderView(10));
foreach (var folder in folders)
{
Console.WriteLine($"Folder: {folder.DisplayName}");
}
}
catch (MsalException ex)
{
Console.WriteLine($"Error acquiring access token: {ex}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex}");
}
Permissions
The solution to the issue is to grant full_access_as_app permission from Office 365 Online Exchange API in Azure Active Directory for the app that will be accessing the mailboxes.
Related
I'm struggeling to understand how to use SharePoint CSOM to add a user as Site Collection Admin.
So far this code works for a Global Admin to add a user as Site Colleciton Admin, eventhough the Global Admin is not included as Site Admin.
I've tried to run the code as normal user which is only Site Collection Admin, to add another user as Site Colleciton Admin. But then I get some errors:
If I use the SharePoint Admin URL to get the Access Token, then the code crash on row #48 as 401 "Unauthorized"
If I use the Site Collection URL to get the Access Token, I get error when trying to get the acces token saying that Site is doesn't exist on the environment.
If I use the root site URL ("https://domain-admin.sharepoint.com/) to get the Access Token, then the code crash on row #51 as 401 "Unauthorized".
I'm using the PnP.PowerShell code as reference: https://github.com/pnp/powershell/blob/dev/src/Commands/Admin/SetTenantSite.cs#L547-L574
And my process is pretty much the same as here: MSAL AD token not valid with SharePoint Online CSOM
But I don't have clear if it's a issue of access token or the CSOM commands I use.
Does anyone has any idea how to move forward?
btw, I guess if I use the Global Admin account I only need to use tenant.SetSiteAdmin(siteCollection, userEmail, true);. I read somewhere that even for global admin I need EnsureUser(userEmail);, but so far the code seems working without it.
using Microsoft.Identity.Client;
using Microsoft.SharePoint.Client;
namespace ScriptTester
{
internal class Program
{
static async Task Main(string[] args)
{
await AddUserAdmin();
}
public static async Task AddUserAdmin()
{
string siteAdmin = "https://domain-admin.sharepoint.com/";
string siteRoot = "https://domain.sharepoint.com/";
string siteCollection = "https://domain.sharepoint.com/sites/SiteName/";
string userEmail = "_email";
string accessToken = await GetAccessToken(siteRoot);
using (var context = new Microsoft.SharePoint.Client.ClientContext(siteRoot))
{
context.ExecutingWebRequest += (sender, e) =>
{
e.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + accessToken;
};
var tenant = new Microsoft.Online.SharePoint.TenantAdministration.Tenant(context);
try
{
addLog("Try using tenant context");
tenant.SetSiteAdmin(siteCollection, userEmail, true);
tenant.Context.ExecuteQueryRetry();
}
catch (Exception ex)
{
addLog("Failed using Tenant context");
addLog(ex.Message);
using (var site = tenant.Context.Clone(siteCollection))
{
var user = site.Web.EnsureUser(userEmail);
user.Update();
user.IsSiteAdmin= true;
site.Load(user);
site.ExecuteQueryRetry();
tenant.SetSiteAdmin(siteCollection, userEmail, true);
tenant.Context.ExecuteQueryRetry();
}
}
}
}
public static async Task<string> GetAccessToken(string siteUrl)
{
string tenantId = "xxxx-xxxx-xxxx-xxxx-xxx";
string clientId = "xxxx-xxxx-xxxx-xxxx-xxx";
Uri authority = new Uri($"https://login.microsoftonline.com/{tenantId}");
string redirectUri = "http://localhost";
string defaultPermissions = siteUrl + "/.default";
string[] scopes = new string[] { defaultPermissions };
var app = PublicClientApplicationBuilder.Create(clientId)
.WithAuthority(authority)
.WithRedirectUri(redirectUri)
.Build();
AuthenticationResult result;
result = await app.AcquireTokenInteractive(scopes)
.WithUseEmbeddedWebView(false)
.ExecuteAsync();
return result.AccessToken;
}
}
}
I will recommend you to use PnP Core component to access site collection and add admin. Please refer to the following code
string siteUrl = "https://xxx.sharepoint.com/";
string userName = "xxxx#xxx.onmicrosoft.com";
string password = "*******";
AuthenticationManager authManager = new AuthenticationManager();
try
{
using (var clientContext = authManager.GetSharePointOnlineAuthenticatedContextTenant(siteUrl, userName, password))
{
List<UserEntity> admins = new List<UserEntity>();
UserEntity admin = new UserEntity();
admin.LoginName = "nirmal";
admins.Add(admin);
clientContext.Site.RootWeb.AddAdministrators(admins, true);
Console.WriteLine("User added as Site Collection Admin");
Console.ReadKey();
}
}
catch (Exception ex)
{
Console.WriteLine("Error Message: " + ex.Message);
Console.ReadKey();
}
Is there any way to authenticate an application with SharePoint rest API like Graph API using Client ID and Client Secret? I wanna use SharePoint rest API in my console application.
This works for me:
In Azure Active Directory I have configured permissions for SharePoint API.
In my code I have scopes defined this way:
var scopes = new [] {"https://<tenantName>.sharepoint.com/allsites.manage"};
var clientApp = ConfidentialClientApplicationBuilder.Create($"{clientId}")
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.WithClientSecret($"{clientSecret}").Build();
// acquire a token for the app
AuthenticationResult result = null;
try
{
result = await clientApp.AcquireTokenForClient(scopes)
.ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
...
}
catch (MsalServiceException ex)
{
...
}
I am using a windows application to upload and download a file from onedrive api.
Code to retrieve token (This code is directly downloaded from azure portal after creating an app registration)
string graphAPIEndpoint = "https://graph.microsoft.com/v1.0/me";
string[] scopes = new string[] { "user.read" };
AuthenticationResult authResult = null;
var app = App.PublicClientApp;
ResultText.Text = string.Empty;
TokenInfoText.Text = string.Empty;
var accounts = await app.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
try
{
authResult = await app.AcquireTokenSilent(scopes, firstAccount)
.ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}");
try
{
authResult = await app.AcquireTokenInteractive(scopes)
.WithAccount(firstAccount)
.WithParentActivityOrWindow(new WindowInteropHelper(this).Handle) // optional, used to center the browser on the window
.WithPrompt(Prompt.SelectAccount)
.ExecuteAsync();
}
catch (MsalException msalex)
{
ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}";
}
}
This is the code to get the download url of an item from onedrive api
string url="https://graph.microsoft.com/v1.0/me/drive/root:/Qwerty/test.txt";
string token=authResult.AccessToken;
var httpClient = new System.Net.Http.HttpClient();
System.Net.Http.HttpResponseMessage response;
try
{
var request = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, url);
//Add the token in Authorization header
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
response = await httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
Response cls = new Response();
cls.Success = "TRUE";
cls.Method = "GetAllFiles";
cls.Data = content;
return cls;
}
else
{
Response cls = new Response();
cls.Success = "FALSE";
cls.Method = "GetAllFiles";
cls.Data = content;
return cls;
}
Im getting this error "Must be authenticated to use '/drive' syntax". this app works with one of my personal app registration . but when i use the below app registration its strating to show this error. i followed the exact same steps in creating the app registration i dnt knw why this error.
client id with error: 463921cd-72a3-495d-847e-259b99dda89e
Please help me
This is the sreenshot
If you would like to download the contents of a DriveItem, you must add one of the following permissions to call this API. And you need to add the delegated permission when using /me. The scope in your code is also changed with permission.
Then you could refer to the code sample:
C# with graph SDK: https://stackoverflow.com/a/63806689/13308381
Try to follow these steps in Postman:
1.Request an authorization code
GET https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?client_id={client_id}&scope={scope}
&response_type=code&redirect_uri={redirect_uri}
2.Request an access token
POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
client_id={client_id}&scope={scope}&redirect_uri={redirect_uri}&client_secret={client_secret}
&code={code}&grant_type=authorization_code
3.Call Microsoft Graph API
GET /me/drive/root:/{item-path}:/content
I have found out the issue. issue was when creating an access token we need specify scopes like(Files.Read,Files.ReadWrite,etc (add whatever we need)) and then use the same token for downloading's a file and it was ok
I have a WEB API application working via Azure active directory.
I can get the information of all the user in active directory like this:
var app = ConfidentialClientApplicationBuilder.CreateWithApplicationOptions(_applicationOptions).Build();
string[] scopes = { "https://graph.microsoft.com/.default" };
AuthenticationResult result = null;
try
{
result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
}
catch (MsalServiceException ex)
{
// Case when ex.Message contains:
// AADSTS70011 Invalid scope. The scope has to be of the form "https://resourceUrl/.default"
// Mitigation: change the scope to be as expected
}
// use the default permissions assigned from within the Azure AD app registration portal
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.GetAsync("https://graph.microsoft.com/v1.0/users");
string content = await response.Content.ReadAsStringAsync();
But if I try to get tenants calling
https://management.azure.com/tenants?api-version=2019-06-01
I receive AuthenticationFailed error.
I guess this is because my AccessToken doesn't have the necessary scopes.
How can I fix it?
You are getting an access token for MS Graph API, not Azure Management API.
Use the following scope:
https://management.core.windows.net/.default
Docs:
https://learn.microsoft.com/en-us/rest/api/azure/#authorization-code-grant-interactive-clients
I'm playing with OneDrive SDK 1.1.15.0:
try
{
AppConfig appConfig = new AppConfig
{
MicrosoftAccountAppId = oneDriveClientID, //something like 00000000123456AB
MicrosoftAccountClientSecret = oneDriveClientSecret, //something like 3vx[...]1sJ
MicrosoftAccountReturnUrl = "https://localhost/return",
MicrosoftAccountScopes = new string[] { "wl.signin", "wl.offline_access", "onedrive.readonly" }
};
OneDriveClient oneDriveClient = new OneDriveClient(appConfig);
AccountSession accountSession = await oneDriveClient.AuthenticateAsync();
//more code
await oneDriveClient.SignOutAsync();
}
catch (Exception ex)
{
throw ex;
}
My problem is in line:
AccountSession accountSession = await oneDriveClient.AuthenticateAsync();
that throws the following exception:
Microsoft.OneDrive.Sdk.OneDriveException, AuthenticationFailure: Failed to retrieve a valid authentication token for the user.
Any ideas?
Thank you in advance!
UPDATE
After reading comment from ginach (thank you!), I update my code. Some arguments to underline:
I want to access OneDrive from an Azure worker Role, so no authentication windows or something like that.
I upload the Microsoft.OneDrive SDK to 1.1.20 version.
I already registered my application to the OneDrive dev portal.
My actual code is:
try
{
MicrosoftAccountServiceInfo serviceInfo = new MicrosoftAccountServiceInfo();
serviceInfo.AppId = oneDriveClientID; //something like: 00000000ABCDEFGH
serviceInfo.ClientSecret = oneDriveClientSecret; //something like: 3vx[...]1sJ
serviceInfo.ReturnUrl = oneDriveReturnUrl; //something like: https://localhost/return
serviceInfo.Scopes = oneDriveAccountScopes; //something like new string[] { "wl.signin", "wl.offline_access", "onedrive.readonly" }
MicrosoftAccountAuthenticationProvider authenticationProvider = new MicrosoftAccountAuthenticationProvider(serviceInfo);
OneDriveClient oneDriveClient = await OneDriveClient.GetAuthenticatedMicrosoftAccountClient(oneDriveClientID, oneDriveReturnUrl, oneDriveAccountScopes, authenticationProvider);
//more code
await oneDriveClient.SignOutAsync();
}
catch (OneDriveException odex)
{
throw odex;
}
catch (Exception ex)
{
throw ex;
}
I obtain again and again (in OneDriveClient.GetAuthenticatedMicrosoftAccountClient method) a OneDriveException stating (Error property): AuthenticationFailure - Failed to retrieve a valid authentication token for the user.
Any suggestion?
Thank you.
UPDATE 2
OK, I'm trying a new approach. Using RestSharp I try to login to OneDrive with that code:
string clientId = "00[...]00";
string scopes = "wl.signin, wl.offline_access, onedrive.readonly";
string responseType = "code";
string redirectUri = "https://login.live.com/oauth20_desktop.srf";
RestClient client = new RestClient("https://login.live.com");
RestRequest request = new RestRequest();
request.Method = Method.GET;
request.Resource = "oauth20_authorize.srf";
request.AddQueryParameter("client_id", clientId);
request.AddQueryParameter("scope", scopes);
request.AddQueryParameter("response_type", responseType);
request.AddQueryParameter("redirect_uri", redirectUri);
IRestResponse response = client.Execute(request);
string content = response.Content;
I check the request with Fiddler and what I'm sending is:
https://login.live.com/oauth20_authorize.srf?client_id=00[...]00&scope=wl.signin%20wl.offline_access%20onedrive.readonly&response_type=code&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf
But OneDrive server answers my with:
Microsoft account requires JavaScript to sign in. This web browser either does not support JavaScript, or scripts are being blocked. To find out whether your browser supports JavaScript, or to allow scripts, see the browser's online help.
So I try the request in a browser and OneDrive server redirects me to the authorization page:
Now the question is: is there any workaround to skip the manual authorization?
Thank you,
Attilio
The client requires an authentication provider to be able to retrieve authentication tokens. There are a few ways to do this depending on your current platform.
Create your own IAuthenticationProvider implementation. The authentication provider is responsible for setting the Authentication header on requests. Here's how you would create a client instance with a custom authentication provider:
var client = new OneDriveClient(appConfig, serviceInfoProvider: new
ServiceInfoProvider(new CustomAuthenticationProvider()));
Use one of the various default authentication implementations. Take a look at the SDK authentication documentation for the available options and examples.
If you have a refresh token and only want to do the silent authentication flow you can use OneDriveClient.GetSilentlyAuthenticatedMicrosoftAccountClient. Here's an example:
var client = await OneDriveClient.GetSilentlyAuthenticatedMicrosoftAccountClient(clientId, returnUrl, scopes, refreshToken);