I created a C# console application to send email using Microsoft Graph API. On adding Mail.Send Application Permission, it works fine. But, because of company requirements, I was asked to use Mail.Send Delegated Permission instead and with that permission I don't see it working and I see this error:
Are there any steps I should consider doing after adding Mail.Send Delegated Permission in order to get this working?
Here is my code:
static void Main(string[] args)
{
// Azure AD APP
string clientId = "<client Key Here>";
string tenantID = "<tenant key here>";
string clientSecret = "<client secret here>";
Task<GraphServiceClient> callTask = Task.Run(() => SendEmail(clientId, tenantID, clientSecret));
// Wait for it to finish
callTask.Wait();
// Get the result
var astr = callTask;
}
public static async Task<GraphServiceClient> SendEmail(string clientId, string tenantID, string clientSecret)
{
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantID)
.WithClientSecret(clientSecret)
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
var message = new Message
{
Subject = subject,
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = content
},
ToRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress { Address = recipientAddress }
}
}
};
var saveToSentItems = true;
await _graphClient.Users[<userprincipalname>]
.SendMail(message, saveToSentItems)
.Request()
.PostAsync();
return graphClient;
}
UPDATE:
Based on below answer, I updated code as follows:
var publicClientApplication = PublicClientApplicationBuilder
.Create("<client-id>")
.WithTenantId("<tenant-id>")
.Build();
var authProvider = new UsernamePasswordProvider(publicClientApplication);
var secureString = new NetworkCredential("", "<password>").SecurePassword;
User me = await graphClient.Me.Request()
.WithUsernamePassword("<username>", secureString)
.GetAsync();
I enabled "Allow public client flows" to fix an exception.
And now I see another exception: Insufficient privileges to complete the operation.
What am I missing?
UPDATE: Currently I see this exception with no changes in the code:
The code you provided shows you use client credential flow to do the authentication. When you use Mail.Send Application permission, use client credential flow is ok. But if you use Mail.Send Delegated permission, we can not use client credential. You should use username/password flow to do authentication.
=================================Update===================================
Below is my code:
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;
using System;
using System.Collections.Generic;
using System.Security;
namespace ConsoleApp34
{
class Program
{
static async System.Threading.Tasks.Task Main(string[] args)
{
Console.WriteLine("Hello World!");
var publicClientApplication = PublicClientApplicationBuilder
.Create("client id")
.WithTenantId("tenant id")
.Build();
string[] scopes = new string[] { "mail.send" };
UsernamePasswordProvider authProvider = new UsernamePasswordProvider(publicClientApplication, scopes);
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
var message = new Message
{
Subject = "Meet for lunch?",
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = "The new cafeteria is open."
},
ToRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "to email address"
}
}
}
};
var securePassword = new SecureString();
foreach (char c in "your password")
securePassword.AppendChar(c);
var saveToSentItems = true;
await graphClient.Me
.SendMail(message, saveToSentItems)
.Request().WithUsernamePassword("your email", securePassword)
.PostAsync();
}
}
}
The reason for your error message Insufficient privileges to complete the operation is you use the code:
User me = await graphClient.Me.Request()
.WithUsernamePassword("<username>", secureString)
.GetAsync();
This code is used to get the user(me)'s information but not send email, you haven't added the permission to the app. So it will show Insufficient privileges to complete the operation. Please remove this code and use the code block in my code instead:
await graphClient.Me.SendMail(message, saveToSentItems)
.Request().WithUsernamePassword("your email", securePassword)
.PostAsync();
==============================Update2====================================
Related
Questions
First question, what determines if an sid claim is emitted from identityserver?
Second question, do I even need an sid? I currently have it included because it was in the sample..
Backstory
I have one website that uses IdentityServer4 for authentication and one website that doesn't. I've cobbled together a solution that allows a user to log into the non-identityserver4 site and click a link that uses one-time-access codes to automatically log into the identityserver4 site. Everything appears to work except the sid claim isn't passed along from identityserver to the site secured by identityserver when transiting from the non-identityserver site. If I log directly into the identityserver4 secured site the sid is included in the claims. Code is adapted from examples of automatically logging in after registration and/or impersonation work flows.
Here is the code:
One time code login process in identityserver4
public class CustomAuthorizeInteractionResponseGenerator : AuthorizeInteractionResponseGenerator
{
...
//https://stackoverflow.com/a/51466043/391994
public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request,
ConsentResponse consent = null)
{
string oneTimeAccessToken = request.GetAcrValues().FirstOrDefault(x => x.Split(':')[0] == "otac");
string clientId = request.ClientId;
//handle auto login handoff
if (!string.IsNullOrWhiteSpace(oneTimeAccessToken))
{
//https://benfoster.io/blog/identity-server-post-registration-sign-in/
oneTimeAccessToken = oneTimeAccessToken.Split(':')[1];
OneTimeCodeContract details = await GetOTACFromDatabase(oneTimeAccessToken);
if (details.IsValid)
{
UserFormContract user = await GetPersonUserFromDatabase(details.PersonId);
if (user != null)
{
string subjectId = await GetClientSubjectIdAsync(clientId, user.AdUsername);
var iduser = new IdentityServerUser(subjectId)
{
DisplayName = user.AdUsername,
AuthenticationTime = DateTime.Now,
IdentityProvider = "local",
};
request.Subject = iduser.CreatePrincipal();
//revoke token
bool? success = await InvalidateTokenInDatabase(oneTimeAccessToken);
if (success.HasValue && !success.Value)
{
Log.Debug($"Revoke failed for {oneTimeAccessToken} it should expire at {details.ExpirationDate}");
}
//https://stackoverflow.com/a/56237859/391994
//sign them in
await _httpContextAccessor.HttpContext.SignInAsync(IdentityServerConstants.DefaultCookieAuthenticationScheme, request.Subject, null);
return new InteractionResponse
{
IsLogin = false,
IsConsent = false,
};
}
}
}
return await base.ProcessInteractionAsync(request, consent);
}
}
Normal Login flow when logging directly into identityserver4 secured site (from sample)
public class AccountController : Controller
{
/// <summary>
/// Handle postback from username/password login
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
Log.Information($"login request from: {Request.HttpContext.Connection.RemoteIpAddress.ToString()}");
if (ModelState.IsValid)
{
// validate username/password against in-memory store
if (await _userRepository.ValidateCredentialsAsync(model.Username, model.Password))
{
AuthenticationProperties props = null;
// only set explicit expiration here if persistent.
// otherwise we reply upon expiration configured in cookie middleware.
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
var clientId = await _account.GetClientIdAsync(model.ReturnUrl);
// issue authentication cookie with subject ID and username
var user = await _userRepository.FindByUsernameAsync(model.Username, clientId);
var iduser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.UserName
};
await HttpContext.SignInAsync(iduser, props);
// make sure the returnUrl is still valid, and if yes - redirect back to authorize endpoint
if (_interaction.IsValidReturnUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
return Redirect("~/");
}
ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
var vm = await _account.BuildLoginViewModelAsync(model);
return View(vm);
}
}
AuthorizationCodeReceived in identityserver4 secured site
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(
tokenEndpoint,
electionClientId,
electionClientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(
new Uri(userInfoEndpoint).ToString());
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
Claim subject = userInfoResponse.Claims.Where(x => x.Type == "sub").FirstOrDefault();
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(GetRoles(subject.Value, tokenClient, apiResourceScope, apiBasePath));
var transformedClaims = StartupHelper.TransformClaims(userInfoResponse.Claims);
id.AddClaims(transformedClaims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
THIS FAILS -> id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
},
}
});
}
}
Questions again if you don't want to scroll back up
First question, what determines if an sid claim is emitted from identityserver?
Second question, do I even need an sid? I currently have it included because it was in the sample..
I'm using IdentityServer4 Tools to manually create a token:
var token = await _tools.IssueClientJwtAsync(
clientId: "client_id",
lifetime: lifetimeInSeconds,
audiences: new[] { TokenHelper.Audience },
additionalClaims:new [] { new Claim("some_id", "1234") }
);
I wonder if there is a way (using what IdentityServer4 already have) to manually decode and validate the token.
To decode the token right now I'm using JwtSecurityTokenHandler (System.IdentityModel.Tokens.Jwt):
var handler = new JwtSecurityTokenHandler();
var tokenDecoded = handler.ReadJwtToken(token);
It is quite simple so I'm happy to keep this if IdentityServer4 doesn't have an equivalent.
What is more important is the validation of the token. I found and adapt this example that does the job. Here the code from Github:
const string auth0Domain = "https://jerrie.auth0.com/"; // Your Auth0 domain
const string auth0Audience = "https://rs256.test.api"; // Your API Identifier
const string testToken = ""; // Obtain a JWT to validate and put it in here
// Download the OIDC configuration which contains the JWKS
// NB!!: Downloading this takes time, so do not do it very time you need to validate a token, Try and do it only once in the lifetime
// of your application!!
IConfigurationManager<OpenIdConnectConfiguration> configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{auth0Domain}.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdConfig = AsyncHelper.RunSync(async () => await configurationManager.GetConfigurationAsync(CancellationToken.None));
// Configure the TokenValidationParameters. Assign the SigningKeys which were downloaded from Auth0.
// Also set the Issuer and Audience(s) to validate
TokenValidationParameters validationParameters =
new TokenValidationParameters
{
ValidIssuer = auth0Domain,
ValidAudiences = new[] { auth0Audience },
IssuerSigningKeys = openIdConfig.SigningKeys
};
// Now validate the token. If the token is not valid for any reason, an exception will be thrown by the method
SecurityToken validatedToken;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
var user = handler.ValidateToken(testToken, validationParameters, out validatedToken);
// The ValidateToken method above will return a ClaimsPrincipal. Get the user ID from the NameIdentifier claim
// (The sub claim from the JWT will be translated to the NameIdentifier claim)
Console.WriteLine($"Token is validated. User Id {user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value}");
The code above is doing the job. I just wonder if IdentityServer4 has already something "simpler" that just does the token validation as the code above does.
What you are trying to do is called token delegation,
you can implement it using Extension Grants on IDS. Here is sample code from docs
public class DelegationGrantValidator : IExtensionGrantValidator
{
private readonly ITokenValidator _validator;
public DelegationGrantValidator(ITokenValidator validator)
{
_validator = validator;
}
public string GrantType => "delegation";
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var userToken = context.Request.Raw.Get("token");
if (string.IsNullOrEmpty(userToken))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var result = await _validator.ValidateAccessTokenAsync(userToken);
if (result.IsError)
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
// get user's identity
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub").Value;
//Generate a new token manually if needed
//Call another API is needed
context.Result = new GrantValidationResult(sub, GrantType);
return;
}
}
Token validation is done using ITokenValidator in above code, you can use this validator in manual validation as well.
Here is another example.
I am authenticating to the Graph API in my Startup.cs:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = "https://login.microsoftonline.com/common/v2.0",
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false // Setting this to true prevents logging in, and is only necessary on a multi-tenant app.
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = async (context) =>
{
// This block executes once an auth code has been sent and received.
Evar idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithClientSecret(appSecret)
.Build();
var signedInUser = new ClaimsPrincipal(context.AuthenticationTicket.Identity);
var tokenStore = new SessionTokenStore(idClient.UserTokenCache, HttpContext.Current, signedInUser);
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(scopes, context.Code).ExecuteAsync();
var userDetails = await GraphUtility.GetUserDetailAsync(result.AccessToken);
After retrieving this access token, I store it into a class variable. The reason why I do this is so that I can retrieve it for use in one of my services (called by an API controller) that interfaces with the Graph API.
public GraphAPIServices(IDbContextFactory dbContextFactory) : base(dbContextFactory)
{
_accessToken = GraphUtility.GetGraphAPIAccessToken();
_graphClient = new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
}));
}
The problem that I am running into is that after some time, this access token eventually expires. I obviously can't run Startup.cs again so there is no opportunity to retrieve a new access token.
What I would like to know is if it's possible to exchange this expired access token for a new one without the need to request that the user logs in again with their credentials?
i'm working wpf application.I want to delete email from all account in domain.
I'm using service account wide delegetion for this.
i also use here for authentication and other methods. I gave all permission for my admin account.
public GmailService GetService()
{ var certificate = new X509Certificate2(#"xxxxxxxxxxxx-
fc9fcdc65959.p12", "notasecret", X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { GmailService.Scope.MailGoogleCom }
}.FromCertificate(certificate));
GmailService service = new GmailService(new
BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = AppName,
});
return service;
}
List Function is below.
public static List<Google.Apis.Gmail.v1.Data.Message>
ListMessages(GmailService service, String userId, String query)
{
List<Google.Apis.Gmail.v1.Data.Message> result = new
List<Google.Apis.Gmail.v1.Data.Message>();
UsersResource.MessagesResource.ListRequest request =
service.Users.Messages.List(userId);
request.Q = query;
do
{
try
{
ListMessagesResponse response = request.Execute();
result.AddRange(response.Messages);
request.PageToken = response.NextPageToken;
}
catch (Exception e)
{
Console.WriteLine("An error occurred: " + e.Message);
}
} while (!String.IsNullOrEmpty(request.PageToken));
return result;
}
When i try to list all emails, i'm getting this error.
"Google.Apis.Requests.RequestError
Bad Request [400]
Errors [
Message[Bad Request] Location[ - ] Reason[failedPrecondition]
Domain[global]
]"
İs anyone there to help me?
You need to add a user account like:
ServiceAccountCredential.Initializer constructor =
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
user = user_email;
Scopes = new[] { GmailService.Scope.MailGoogleCom }
}.FromCertificate(certificate));
I've created a C# function in Azure and it looks like this:
using System;
using System.Net;
using System.Net.Http.Headers;
using System.Collections.Specialized;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Newtonsoft.Json;
using System.Text;
using Newtonsoft.Json.Linq;
public static async void Run(string input, TraceWriter log)
{
log.Info("---- Gestartet ----");
var token = await HttpAppAuthenticationAsync();
log.Info("---- Token: " + token.ToString());
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var user = "username#XXXXX.com";
var userExists = await DoesUserExistsAsync(client, user, log);
if(userExists)
{
log.Info("Der Benutzer existiert.");
}
else {
log.Info("Benutzer nicht gefunden.");
}
}
public static async Task<string> HttpAppAuthenticationAsync()
{
//log.Info("---- Start ----");
// Constants
var tenant = "2XXXXXCC6-c789-41XX-9XXX-XXXXXXXXXX";
var resource = "https://graph.windows.net/";
var clientID = "5XXXXef-4905-4XXf-8XXa-bXXXXXXX2";
var secret = "5GFzeg6VyrkJYUJ8XXXXXXXeKbjYaXXX7PlNpFkkg=";
var webClient = new WebClient();
var requestParameters = new NameValueCollection();
requestParameters.Add("resource", resource);
requestParameters.Add("client_id", clientID);
requestParameters.Add("grant_type", "client_credentials");
requestParameters.Add("client_secret", secret);
var url = $"https://login.microsoftonline.com/{tenant}/oauth2/token";
var responsebytes = await webClient.UploadValuesTaskAsync(url, "POST", requestParameters);
var responsebody = Encoding.UTF8.GetString(responsebytes);
var obj = JsonConvert.DeserializeObject<JObject>(responsebody);
var token = obj["access_token"].Value<string>();
//log.Info("HIER: " + token);
return token;
}
private static async Task<bool> DoesUserExistsAsync(HttpClient client, string user, TraceWriter log)
{
log.Info("---- Suche Benutzer ----");
try
{
var payload = await client.GetStringAsync($"https://graph.microsoft.net/v1.0/users/user");
return true;
}
catch (HttpRequestException)
{
return false;
}
}
In my log I get the bearer token. But then result of DoesUserExistsAsync is false.
If I send an request via Postman with the token I get following response:
{
"error": {
"code": "Authorization_RequestDenied",
"message": "Insufficient privileges to complete the operation.",
"innerError": {
"request-id": "10XXX850-XXX-4d72-b6cf-78X308XXXXX0",
"date": "2017-09-07T14:03:58"
}
}
}
In the Azure AD I created an App and the permissions are:
(I gave all permisions only to test what`s wrong)
Since you're using client_credentials, there is no "user". That OAUTH grant only authenticates your application, not an actual user.
When using client_credentials, only the scopes listed under "Application Permissions" are applicable. Since you don't have a user authenticated, there isn't a user to "delegate" to your app.
Application Permissions are also unique in that every one of them requires Admin Consent before your app can use them. Without consent, your application will have insufficient privileges to complete any operation.
Also, this call won't return anything:
await client.GetStringAsync($"https://graph.microsoft.net/v1.0/users/user");
I assume what you're really looking for is:
private async Task<bool> DoesUserExistsAsync(HttpClient client, string userPrincipalName, TraceWriter log)
{
log.Info("---- Suche Benutzer ----");
try
{
var payload = await client.GetStringAsync($"https://graph.microsoft.net/v1.0/users/"
+ userPrincipalName);
return true;
}
catch (HttpRequestException)
{
return false;
}
}