My scenario is to create app & spn via AAD Graph. That ist rather easy (with a redirect for browser-based consent), what I now want to do is consent the spn right away (like you can do in the portal). The code itself is straight-forward:
var g = new OAuth2PermissionGrant();
g.ClientId = thePrincipal.ObjectId;
g.ConsentType = "AllPrincipals";
g.PrincipalId = null;
g.ResourceId = ##resourceId##;
g.ExpiryTime = DateTime.Now.AddYears(10);
g.Scope = "User.Read";
await client.Oauth2PermissionGrants.AddOAuth2PermissionGrantAsync(g);
Now the part that I haven't figured out properly is ##resourceId##. This is supposed to be the resourceId - in the code sample, it should be Windows Azure Active Directory. How do I get the resourceId for eg the following required resource access (00000002-0000-0000-c000-000000000000):
RequiredResourceAccess =
new [] {
new RequiredResourceAccess() {
ResourceAppId = "00000002-0000-0000-c000-000000000000",
ResourceAccess = new [] {
new ResourceAccess() {
Id = new Guid("311a71cc-e848-46a1-bdf8-97ff7156d8e6"), // sign in and read profile (delegated perm)
Type = "Scope"
},
The lookup ResourceAppId -> resourceId (app to spn) is what I am missing. For eg AAD, Graph, manage.office.com et al.
From the documentation for the OAuth2PermissionGrant entity, the resourceId field of an OAuth2PermissionGrant is the objectId of the ServicePrincipal object for the resource:
Specifies the objectId of the resource service principal to which access has been granted.
So, from the tenant in which you are creating the OAuth2PemrissionGrant, you need to retrieve the ServicePrincipal object corresponding to the resource app you would like to grant permission to, and from that object, read the objectId property.
If you have the resource app's AppId, you can retrieve the corresponding ServicePrincipal object (if one exists) with:
GET https://graph.windows.net/{tenant}/servicePrincipals
?$filter=appId eq '{app-id-guid}'
&api-version=1.6
With Microsoft.Azure.ActiveDirectory.GraphClient (which I think is what you're using in your code), you would do this with:
graphClient.ServicePrincipals.Where(sp => sp.AppId == "{app-id-guid}")
If what you have to identify the resource app is not the Guid app ID, but a (somewhat) friendly identifier URI (e.g. "https://graph.microsoft.com"), you can retrieve the matching ServicePrincipal object by filtering on servicePrincipalNames.
With Azure AD Graph:
GET https://graph.windows.net/{tenant}/servicePrincipals
?$filter=servicePrincipalNames/any(n:n eq 'https://graph.microsoft.com'))
&api-version=1.6
With Microsoft.Azure.ActiveDirectory.GraphClient:
graphClient.ServicePrincipals
.Where(sp => sp.ServicePrincipalNames.Any(n => n == "https://graph.microsoft.com"))
Related
I am working on an Azure AD B2C application and the B2C policy stores the MFA secret-key in the extension_mfaTotpSecretKey property of the user. This works and when I run Get-AzureADUser -ObjectId '<object-id>' | ConvertTo-Json, then it shows:
{
"ExtensionProperty": {
"odata.metadata": "https://graph.windows.net/<tenant-id>/$metadata#directoryObjects/#Element",
"odata.type": "Microsoft.DirectoryServices.User",
"createdDateTime": "2/4/2022 2:13:22 PM",
"employeeId": null,
"onPremisesDistinguishedName": null,
"userIdentities": "[]",
"extension_7eb927869ae04818b3aa16db92645c09_mfaTotpSecretKey": "32YZJFPXXOMHT237M64IVW63645GXQLV"
},
"DeletionTimestamp": null,
...
}
During the migration process from the old directory to the new Azure B2C directory, I also want to transfer the existing TOTP key so users don't need to reregister their TOTP key. I have spent all day to get this to work, but no luck and I really don't know what's left.
I have created an app registration in the tenant with Directory.ReadWrite.All rights, but when I read the user, then the extension is empty:
var creds = new ClientSecretCredential("<tenant-id>", "<client-id>", "<client-secret>", new TokenCredentialOptions { AuthorityHost = AzureAuthorityHosts.AzurePublicCloud });
var graphClient = new GraphServiceClient(creds, new[] { "https://graph.microsoft.com/.default" });
var user = await graphClient.Users["<object-id>"].Request().Select(u => new {u.Id, u.Extensions}).Expand(u => new { u.Extensions}).GetAsync();
If I can't read the value, then I probably can't write it. I tried using OpenTypeExtension, but I am under the impression that this is a completely different property.
I can't find any documentation that tells me how I can run Get-AzureADUser using Graph API v2 in C#.
It seems that there three possible ways to extend properties in Azure AD for an object:
AzureAD Graph extension attributes
Azure AD Open extensions
Azure AD Schema extensions
Azure B2C uses AzureAD Graph extension attributes and these should be fetched directly on the user object like this:
var graphClient = new GraphServiceClient(...);
var user = await graphClient.Users["<object-id>"].Request().Select("extension_7eb927869ae04818b3aa16db92645c09_mfaTotpSecretKey").GetAsync();
var mfaTotpSecretKey = user.AdditionalData["extension_7eb927869ae04818b3aa16db92645c09_mfaTotpSecretKey"]?.ToString();
When the user is created, then these properties can be added to the AdditionalData property of the user.
Note that Azure B2C uses the persistent claim name extension_mfaTotpSecretKey, but this is translated to extension_<client-id-without-hyphens>_mfaTotpSecretKey, where <client-id> is the client-id of the B2C extensions app (with all hyphens removed).
Extension attributes are not included by default if you use the v1 endpoint of the Microsoft Graph. You must explicitly ask for them via a $select, as per #Ramon answer. When you use a $select statement, you'll get back only the specified attributes plus the id, so pay attention and specify all the fields you need. Moreover, the SDK is misleading since you'll find the extension attributes under the AdditionalData field, not in the Extensions field.
When you are going to migrate the users to a new tenant, keep in mind that the extension attribute name will change since the middle part is the b2c-extensions appId without the dashes.
i.e.
on Tenant 1: extension_xxx_myAttribute
on Tenant 2: extension_yyy_myAttribute
When you'll try to write the extension attribute on Tenant 2 via Microsoft Graph it must already exist. If you never run your custom policies on the new tenant you can create the attribute via Microsoft Graph as well with a simple POST operation:
POST https://graph.microsoft.com/v1.0/applications/<b2c-extensions-app-objectId/extensionProperties
{
"name": "attributeName",
"dataType":"string/int/etc.",
"targetObjects": ["User"]
}
You'll get the full extension attribute name in the response (i.e. extension_xxx_attributeName)
HTH, F.
https://learn.microsoft.com/en-us/graph/api/application-list-extensionproperty?view=graph-rest-1.0&tabs=csharp
GraphServiceClient graphClient = new GraphServiceClient( authProvider );
var extensionProperties = await graphClient.Applications["{application-id}"].ExtensionProperties
.Request()
.GetAsync();
I am attempting to update a user's AppRole assignments via the Graph Client. As per MS documents I am attempting to do it from the service principal side rather than the user side.
var sp = await _graphServiceClient.ServicePrincipals[objectId].Request().GetAsync();
ServicePrincipal newSp = new ServicePrincipal
{
Id = objectId,
AppId = _configuration["AzureAd:AppId"]
};
newSp.AppRoleAssignedTo = new ServicePrincipalAppRoleAssignedToCollectionPage();
newSp.AppRoleAssignedTo.Add(new AppRoleAssignment
{
PrincipalId = new Guid(u.Id),
ResourceId = new Guid(objectId),
AppRoleId = new Guid(r)
});
await _graphServiceClient.ServicePrincipals[objectId].Request().UpdateAsync(newSp);
I am getting 'One or more property values specified are invalid' but of course no real info on what property or even which object is the problem.
Anyone see anything obvious? I'm guessing on the syntax for the client usage bc I don't see much documentation or examples for it.
I test with same code with yours and met same issue and do some modification but still can't solve the issue. For your requirement of update user's AppRole assignment, I'm not sure if we can do it by the code you provided, but I can provide another solution which is more directly.
The code you provided is new a service principal and add the role assignment into it, then update the service principal. Here provide another solution, it can add the app role assignment directly:
var appRoleAssignment = new AppRoleAssignment
{
PrincipalId = Guid.Parse("{principalId}"),
ResourceId = Guid.Parse("{resourceId}"),
AppRoleId = Guid.Parse("{appRoleId}")
};
await graphClient.Users["{userId}"].AppRoleAssignments
.Request()
.AddAsync(appRoleAssignment);
The code above request this graph api in backend.
I'm trying to add optional claims using Microsoft Identity Web - NuGet for user authentication in NET Core 3.1 WebApp. Reading the MS Docs, it seems that the only steps needed are to declare the optional claims within the App Registration Manifest file in Azure. But when testing the login process using two different apps (my own code and an MS project example) it looks like the optional claims are not being added to the ID Token when returned from Azure following a successful login i.e they're not present at all when viweing the token details in Debug.
I'm not sure how to diagnose this and where to trace the issue i.e am I missing any required steps in Azure setup?
Side Note: Just to confirm it is the jwt ID Token I want to receive the additional claims, NOT the jwt access token used for calling the graph or another Web API endpoint.
MS Docs reference: v2.0-specific optional claims set
Below is the extract from the Manifest file: (note I've even declared the "accessTokenAcceptedVersion": 2, given that optional claims I'm using are not available in ver.1, which if the above was left at default 'null' value then Azure will assume we're using legacy ver.1 - a possible gotcha)
"accessTokenAcceptedVersion": 2,
"optionalClaims": {
"idToken": [
{
"name": "given_name",
"source": "user",
"essential": false,
"additionalProperties": []
},
{
"name": "family_name",
"source": "user",
"essential": false,
"additionalProperties": []
}
],
"accessToken": [],
"saml2Token": []
},
Extract from startup class:
public void ConfigureServices(IServiceCollection services)
{
// Added to original .net core template.
// ASP.NET Core apps access the HttpContext through the IHttpContextAccessor interface and
// its default implementation HttpContextAccessor. It's only necessary to use IHttpContextAccessor
// when you need access to the HttpContext inside a service.
// Example usage - we're using this to retrieve the details of the currrently logged in user in page model actions.
services.AddHttpContextAccessor();
// DO NOT DELETE (for now...)
// This 'Microsoft.AspNetCore.Authentication.AzureAD.UI' library was originally used for Azure Ad authentication
// before we implemented the newer Microsoft.Identity.Web and Microsoft.Identity.Web.UI NuGet packages.
// Note after implememting the newer library for authetication, we had to modify the _LoginPartial.cshtml file.
//services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
// .AddAzureAD(options => Configuration.Bind("AzureAd", options));
///////////////////////////////////
// Add services required for using options.
// e.g used for calling Graph Api from WebOptions class, from config file.
services.AddOptions();
// Add service for MS Graph API Service Client.
services.AddTransient<OidcConnectEvents>();
// Sign-in users with the Microsoft identity platform
services.AddSignIn(Configuration);
// Token acquisition service based on MSAL.NET
// and chosen token cache implementation
services.AddWebAppCallsProtectedWebApi(Configuration, new string[] { Constants.ScopeUserRead })
.AddInMemoryTokenCaches();
// Add the MS Graph SDK Client as a service for Dependancy Injection.
services.AddGraphService(Configuration);
///////////////////////////////////
// The following lines code instruct the asp.net core middleware to use the data in the "roles" claim in the Authorize attribute and User.IsInrole()
// See https://learn.microsoft.com/aspnet/core/security/authorization/roles?view=aspnetcore-2.2 for more info.
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// The claim in the Jwt token where App roles are available.
options.TokenValidationParameters.RoleClaimType = "roles";
});
// Adding authorization policies that enforce authorization using Azure AD roles. Polices defined in seperate classes.
services.AddAuthorization(options =>
{
options.AddPolicy(AuthorizationPolicies.AssignmentToViewLogsRoleRequired, policy => policy.RequireRole(AppRole.ViewLogs));
});
///////////////////////////////////
services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
// Adds the service for creating the Jwt Token used for calling microservices.
// Note we are using our independant bearer token issuer service here, NOT Azure AD
services.AddScoped<JwtService>();
}
Sample Razor PageModel method:
public void OnGet()
{
var username = HttpContext.User.Identity.Name;
var forename = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "given_name")?.Value;
var surname = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "family_name")?.Value;
_logger.LogInformation("" + username + " requested the Index page");
}
UPDATE
Getting closer to a solution but not quite there yet. Couple of issues resolved:
I originally created the Tenant in Azure to use B2C AD, even though I was no longer using B2C and had switched to Azure AD. It wasn't until I deleted the tenant and created a new one before I started to see the optional claims come through to the webapp correctly. After creating the new tenant and assigning the tenant type to use Azure AD, I then found that the 'Token Configuration' menu was now available for configuring the optional claims through the UI, it seems that modifying the App manifest is still required as well, as shown above.
I had to add the 'profile' scope as type 'delegated' to the webapp API Permissions in Azure.
The final issue still unresolved is that although I can see the claims present during Debug, I cant figure out how to retrieve the claim values.
In the method below, I can see the required claims when using Debug, but can't figure out how to retrieve the values:
public void OnGet()
{
var username = HttpContext.User.Identity.Name;
var forename = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "given_name")?.Value;
var surname = HttpContext.User.Claims.FirstOrDefault(c => c.Type == "family_name")?.Value;
_logger.LogInformation("" + username + " requested the Index page");
}
Debug Screenshots shows the given_name & family_name are present:
I've tried different code examples using the claims principal to try and get the values out, but nothing is working for me. Hoping this final riddle is fairly simple to someone who knows the required syntax, as said we now have the required optional claims present, its just not knowing how to actually get the values out.
Big thanks to 'Dhivya G - MSFT Identity' for their assistance (see comments below my original question) method below now allows me to access the required claim values from the Token ID returned from Azure following successful login.
public void OnGet()
{
var username = HttpContext.User.Identity.Name;
var forename = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value;
var surname = HttpContext.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value;
_logger.LogInformation("" + username + " requested the Index page");
}
I have a web application that uses Azure ACS and Azure AD to handle our authentication.
We have a user management feature in the web application that allows a user to create new users. This takes the details such as username, password, email etc. and uses the graph service to create a user in azure.
var newUser = new Microsoft.WindowsAzure.ActiveDirectory.User
{
userPrincipalName = user.UserName,
mailNickname = user.MailNickname,
accountEnabled = true,
displayName = user.FirstName + " " + user.Surname,
givenName = user.FirstName,
surname = user.Surname
};
newUser.passwordProfile = new PasswordProfile
{
forceChangePasswordNextLogin = false,
password = user.Password
};
var graphService = GetGraphService(tenantName);
graphService.AddTousers(newUser);
graphService.SaveChanges();
We are then required to create a record in the web application database for this user. The record needs the object ID from azure. So we use the graphService to get the newly-created user details. This is where my problem lies. It doesn't find the user.
private string GetObjectIdFromAzure(string userName, string tenantName)
{
var graphService = GetGraphService(tenantName);
var users = graphService.users;
QueryOperationResponse<Microsoft.WindowsAzure.ActiveDirectory.User> response;
response = users.Execute() as QueryOperationResponse<Microsoft.WindowsAzure.ActiveDirectory.User>;
var user = response.FirstOrDefault(x => x.userPrincipalName == userName);
return user != null ? user.objectId : "";
}
My code was working without any issues for a few months and only today I am having issues. What frustrates me more it that I have another deployment of the same code where it works without any issues. Some differences between the two deployments are:
The deployments use different Access control namespaces in Azure
The deployments have separate applications in Azure AD
One is https, one is http
The users for both system are under the same Directory.
I have put in logging in both deployments to get the number of users returned by
users.Execute()
In both systems it reported 100 (they share the same users)
Any ideas of what would cause this to stop working? I didn't change any code relating to this recently, I haven't changed any configuration on Azure and I didn't change the web.config of the application
The problem was caused by the fact that I was filtering the users after retrieving them. The graph API was only returning a maximum of 100 users.
So the process was like so:
User created in Azure
Success message returned
Web App searches Azure for user to get Object ID
Graph Api only returns top 100 users. User was not in top 100 alphabetically so error thrown
The reason it was working on our second deployment was that I was prefixing the user name with demo_ (we use this site to demo new features before releasing). This meant that it was being returned in the top 100 users.
I changed the code as follows so it filters during the retrieval instead of after:
private Microsoft.WindowsAzure.ActiveDirectory.User GetUserFromAzure(string userName, string tenantName, out DirectoryDataService graphService)
{
graphService = GetGraphService(tenantName);
var users = (DataServiceQuery<Microsoft.WindowsAzure.ActiveDirectory.User>)graphService.users.Where(x => x.userPrincipalName == userName);
QueryOperationResponse<Microsoft.WindowsAzure.ActiveDirectory.User> response;
response = users.Execute() as QueryOperationResponse<Microsoft.WindowsAzure.ActiveDirectory.User>;
var user = response.FirstOrDefault();
return user;
}
the company I work for has 2 Active Directory forests. One forest is called us where I log on in the morning with my profile (us\maflorin) and another forest is called (mail.us) which is dedicated to Exchange.
I have created an asp.net application that runs on SharePoint and gets the SPContext.Current.Web.CurrentUser.LoginName which is the us domain login name. (us\maflorin for example for me). I would like to get from the us credentials the corresponding object on the Exchange forest in order to write changes to the global address list (GAL) for user that opened the page after a line manager approval process.
I wrote the following working code to get the Exchange object, but it uses two ldap queries to find the object:
private Dictionary<string,AdRecod> FindExchangeAdProperties(string samAccountName,string description)
{
Dictionary<string,AdRecod> properties = null;
if (!string.IsNullOrEmpty(samAccountName))
{
properties = GetUserProperties(#"(&(objectCategory=person)(mailNickname=" +
samAccountName + "))");
if (properties != null) return properties;
}
if ((description == "") || (description == "0"))
throw new Exception("No matching Description, couldn't find correct Exchange AD object");
properties = GetUserProperties(#"(&(objectCategory=person)(description=" +
description + "))");
return properties;
}
Is it possible to get the exchange object with a single ldap query directly from the us samAccountName?
The mailNickname attribute on the exchange forest does not always match the sAMAccountName on the us forest. If it does not match, I use the second ldap query to see if a record is return by querying on the description field. The description field is many times the same for both forests but sometimes an administrator changed it.
Is it possible to find the corresponding Exchange Active Directory object for the us domain credentials more easily? How does Outlook find from the us credentials the corresponding mailbox / Ad object ? I was looking at the AD schema with adsiedit but could not find a clear field that is used to link the two forest objects together.
Furthermore I was looking into the Autodiscover service of the exchange web services managed api (mailbox dn attribute) but you need to pass into the GetUserSettings method an SMTP address and this field is not populated on the us domain.
Many thanks,
Mathias
I was able to find an answer to this question with a better approach than the one above which depends on the company's naming convention.
On the exchange forest I run a LDAP query with the DirectorySearcher class to obtain the attribute msExchMasterAccountSid.
The following code then provides the correct sam on the forest we use to logon:
var sid = directoryEntry.Properties["msExchMasterAccountSid"].Value as byte[];
// no mailbox
if (sid == null) continue;
var sidString = new SecurityIdentifier(sid, 0).ToString();
var samAccountName = "";
using (var context = new PrincipalContext(ContextType.Domain, "US"))
{
var principal = UserPrincipal.FindByIdentity(context, IdentityType.Sid, sidString);
if (principal == null) continue;
samAccountName = principal.SamAccountName;
}