I am trying to read username from Azure AD for a logged-in user using Microsoft Graph API, but I am getting only Name and UserPrincipalName which is some kind of {Guid}-#{tenant}.onmicrosoft.com. Below is my code snippet.
appsetting.json file
"Authentication": {
"AzureAdB2C": {
"Instance": "************",
"ClientId": "********",
"Tenant": "********",
"SignUpSignInPolicyId": "B2C_1_SignUpSignIn",
"ResetPasswordPolicyId": "B2C_1_PasswordReset",
"EditProfilePolicyId": "",
"RedirectUri": "/signin-oidc",
"ClientSecret": "*************"
}
}
public class AzureADProfileResponse
{
public string userPrincipalName { get; set; }
public string surname { get; set; }
public string displayName { get; set; }
public string givenName { get; set; }
}
Get Token for Graph API
public async Task<string> GetTokenAsync()
{
var clientId = _configuration.GetValue<string>("Authentication:AzureAdB2C:ClientId");
var app = PublicClientApplicationBuilder.Create(clientId).Build();
var accounts = await app.GetAccountsAsync();
string[] Scopes = { "User.Read" };
string token = null;
var authResult = await app.AcquireTokenSilent(Scopes, accounts.FirstOrDefault()).ExecuteAsync();
token = authResult.AccessToken;
return token;
}
Reading the config
private AzureTenant GetTenantDetailsFromConfig()
{
return new AzureTenant
{
Tenant = _configuration.GetValue<string>("Authentication:AzureAdB2C:Tenant"),
ClientId = _configuration.GetValue<string>("Authentication:AzureAdB2C:ClientId"),
ClientSecret = _configuration.GetValue<string>("Authentication:AzureAdB2C:ClientSecret")
};
}
Getting profile details for logged-in user
public async Task<AzureADProfileResponse> GetProfileDetails(AzureADProfileRequest azureAd)
{
var appValues = GetTenantDetailsFromConfig();
string url = "https://graph.microsoft.com/v1.0" + "/users" + "/" + azureAd.UserId ;
var accessToken = GetAzureToken(appValues.ClientId, appValues.ClientSecret, appValues.Tenant).Result;
using (HttpClient httpClient = new HttpClient())
{
using (HttpRequestMessage apiRequest = new HttpRequestMessage(HttpMethod.Get, url))
{
apiRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using (HttpResponseMessage apiResponse = httpClient.SendAsync(apiRequest).Result)
{
var apiJsonResponse = await apiResponse.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<AzureADProfileResponse>(apiJsonResponse);
}
}
}
}```
I am getting the other details and able to get UserPrincipalName which is in Guid format, but I require to read the username which is displayed in Azure AD for each of the users. Is there any way by which I can decode/read the displayUserPrincipal name for the logged-in user.
I am able to see the response under network tab in Azure AD for the below request https://main.iam.ad.ext.azure.com/api/Users/{objectId} with the displayUserPrincipalName.
I was getting a [GUID]#[tenant].onmicrosoft.com userPrincipalName, and was looking for the user's actual email address.
I was able to find it in the identities array, which you have to query specifically for in the graph api. It has a convenient signInType: 'emailAddress' attribute so you can easily find it programmatically.
Use the beta endpoint. The username is in the identities collection.
Microsoft Graph only provides MailNickName, UserPrincipalName, and mail.
Please refer the supported/list of Properties returned from Users List in this document.
You can get the UserPrincipalName with the below query
https://graph.microsoft.com/v1.0/users/userid?$select=userPrincipalName
Please refer similar Question
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 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====================================
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));
In an Asp net core MVC application, I use Active Directory for automatic login like this :
this.user = UserPrincipal.FindByIdentity(this.context, Environment.UserName);
and I get groups of the user with this :
public List<String> GetUserGroups()
{
List<String> groups = new List<String>();
foreach(GroupPrincipal gr in user.GetGroups())
{
groups.Add(gr.Name);
}
return groups;
}
And I would like to implement Autorisation with this groups, something like that :
[Authorize(Roles ="Admin")]
public IActionResult OnlyAdmin(){}
with something that link AD groups with authorization Roles or directly check authorization with AD groups if possible but I don't know how to do something like that.
note : I haven't any login/logout pages, it's only automatic.
EDIT
Don't know exactly why or how but it finaly work whithout any code and only with the user login in the PC not the user specified in this.user but it's fine like that.
But now I get a 404 error when I'm trying to access a denied page, why it's not a 401 or 403 error ? How can I redirect a denied access to a custom error page ?
You need to add the group in the ClaimsPrincipal class, i.e.
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, username));
foreach (string userGroup in authResponse)
{
claims.Add(new Claim(ClaimTypes.Role, userGroup, ClaimValueTypes.String,"system","system"));
}
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "authenticationScheme"));
Now use authorize attribute, either on controller or action as :
[Authorize(Roles = "guest,home")]
You can write an ErrorHandlingMiddleware as follows. You will need to register it in the startup file
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
following is an example for the same.
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> createLogger)
{
this._next = next;
this._logger = createLogger;
}
public async Task Invoke(HttpContext context)
{
var statusCode = HttpStatusCode.OK;
try
{
await _next.Invoke(context);
}
catch (Exception ex)
{
this._logger.LogError(ex, ex.Message);
switch (context.Response.StatusCode)
{
case (int)HttpStatusCode.NotFound:
statusCode = HttpStatusCode.NotFound;
break;
case (int)HttpStatusCode.Forbidden:
statusCode = HttpStatusCode.Forbidden;
break;
case (int)HttpStatusCode.BadRequest:
statusCode = HttpStatusCode.BadRequest;
break;
default:
statusCode = HttpStatusCode.InternalServerError;
break;
}
context.Response.StatusCode = (int)statusCode;
}
if (!context.Response.HasStarted)
{
context.Response.ContentType = "application/json";
var response = new { code = statusCode };
var json = JsonConvert.SerializeObject(response);
await context.Response.WriteAsync(json);
}
}
}
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;
}
}