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();
Related
i 'm working on an azure functions that make some graph call to different tenant (multitenant)
I want to reuse a GraphServiceClient and leveraging token cache
I generate the GraphServiceClient in this way:
List<string> scopes = new List<string>() { "https://graph.microsoft.com/.default" };
var authProvider = ConfidentialClientApplicationBuilder.Create("e9b93362-a788-4644-8623-da9f4d4776a7")
.WithAuthority(AzureCloudInstance.AzurePublic, AadAuthorityAudience.AzureAdMultipleOrgs)
.WithClientSecret("fkpx53225awyQJDHV35:^][")
.Build();
var dd = new MsalAuthenticationProvider(authProvider, scopes.ToArray(),"ugochotmail.onmicrosoft.com");
var appGraphClient = new GraphServiceClient(dd);
Than i should call
authResult = await _clientApplication.AcquireTokenForClient(_scopes)
.WithAuthority(AzureCloudInstance.AzurePublic, Tenant)
.ExecuteAsync();
To obtain a token for the app to access the specific tenant.
The problem is in the authentication provider that is call on every send request but doen't offer a parameter with the tenant name
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
var token = await GetTokenAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
}
At the moment i just add a property to the Authentication provider to set the tenant. It works but i would like to know if there is a better solution
Per my understanding, it seems your function doesn't allow a parameter which specify the tenant name and then use the tenant name when do GetTokenAsync() method. And now you can just hardcode the tenant name in the line new MsalAuthenticationProvider(... to specify the tenant.
For this problem, I think you can add a variable named tenant in the "Application settings" of your function app (as below screenshot show).
Then add a line of code string tenant = System.Environment.GetEnvironmentVariable("tenant"); above var token = await GetTokenAsync();
After that, you can add parameter in method GetTokenAsync() like GetTokenAsync(tenant). Then you do not need to hardcode tenant name in code, you just need to change the tenant name in "Application settings" of your function.
If I misunderstand your requirement, please provide more details.
=============================Update===============================
It seems you just want to specify the tenant in your code by a parameter, but not add the tenant name as a property in var dd = new MsalAuthenticationProvider(authProvider, scopes.ToArray(),"tenant name");. If so, you can refer to the code below (just add a line .WithTenantId("xxx.onmicrosoft.com") when do ConfidentialClientApplicationBuilder)
No it doesn't fix the problem as, in a multitenant, the target tenant is send as a parameter to the function. I'm working on an other approach i will come back when i will finish tests.
Thanks a lot
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 created a delegation extension grant the way they did in the docs. (https://identityserver4.readthedocs.io/en/latest/topics/extension_grants.html)
In the example, they get the user's identity from the claims and return the grant validation result like so:
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub").Value;
context.Result = new GrantValidationResult(sub, GrantType);
My issue is that I don't always have a subject aka user identity when I need to utilize the delegation grant. In my scenario, I have an application listening to messages. When the app gets a message, it calls an API using client_credentials. That API then calls a sub API using the delegation grant type. Since the app is using client_credentials, there is no "sub" in the claims.
I tried checking if the "sub" claim exists and if not, set the subject of the GrantValidationResult to a "magical" guid which the IUserStore's FindByIdAsync would look for and either return null or a newed up empty TUser. In both cases, this causes Microsoft.AspNetCore.Identity to bomb futher down the pipeline.
How can I return a GrantValidationResult with the current claims, but not the subject when it doesn't exist?
I found this override for the GrantValidationResult.
// Summary:
// Initializes a new instance of the IdentityServer4.Validation.GrantValidationResult
// class with no subject. Warning: the resulting access token will only contain
// the client identity.
public GrantValidationResult(Dictionary<string, object> customResponse = null);
Since I don't have any custom responses, if "sub" is null, then I do this:
context.Result = new GrantValidationResult(new Dictionary<string, object>());
Doing it this way still populates the claims with the requested/validated scopes.
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"))
I've spent some time getting my MVC 6 .NET Core website working with Azure B2C and everything seems to be working great. However, there are a few questions surrounding claims that I can't seem to figure out the correct strategy.
Say a user signs up on my site with email, firstname, lastname. Once the registration is complete, I would like to add a record into a UserProfile table in my database that references this user.
Question 1:
Should I create a "UserProfileId" claim in Azure B2C? Or should I create an "ObjectId" field in my database table that references the AD user? What would make more sense?
Question 2:
Once a user registers, where and how would I update an AD User claim? Would I do it in one of these events? Or somewhere else? I see there is a "User is new" claim that I could check for?
OnAuthenticationValidated
OnAuthorizationCodeReceived
OnRedirectToAuthenticationEndpoint
Question 3:
To update the claims, would I use: Microsoft.Azure.ActiveDirectory.GraphClient? Does anyone have any sample code for how to update a custom claim? I've tried this but it doesn't seem to persist:
var identity = context.AuthenticationTicket.Principal.Identity as ClaimsIdentity;
identity?.AddClaim(new Claim("EmployeeId", "33"));
Here is my authentication configuration. Thanks!!!!!
public void ConfigureAuth(IApplicationBuilder app, IOptions<PolicySettings> policySettings, AuthenticationHelper authHelper)
{
app.UseCookieAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.AccessDeniedPath = "/Home/Forbidden";
options.CookieSecure = CookieSecureOption.Always;
options.ExpireTimeSpan = TimeSpan.FromHours(1);
options.SlidingExpiration = true;
});
app.UseOpenIdConnectAuthentication(options =>
{
options.PostLogoutRedirectUri = policySettings.Value.PostLogoutRedirectUri;
options.AutomaticAuthenticate = true;
options.AutomaticChallenge = true;
options.ClientId = policySettings.Value.ClientId;
options.CallbackPath = new PathString("/signin-mysite");
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.ResponseType = OpenIdConnectResponseTypes.IdToken;
options.Authority = string.Format(CultureInfo.InvariantCulture, "{0}/{1}", policySettings.Value.AadInstance, policySettings.Value.Tenant);
options.Events = new OpenIdConnectEvents {
OnAuthenticationValidated = OnAuthenticationValidated,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
OnAuthenticationFailed = OnAuthenticationFailed,
OnRedirectToAuthenticationEndpoint = OnRedirectToAuthenticationEndpoint
};
options.ConfigurationManager = new PolicyConfigurationManager(
String.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}/{3}", policySettings.Value.AadInstance, policySettings.Value.Tenant, "v2.0", OpenIdProviderMetadataNames.Discovery),
new string[] { policySettings.Value.SignUpInPolicyId, policySettings.Value.ProfilePolicyId, policySettings.Value.PasswordPolicyId });
});
}
Question 1: Should I create a "UserProfileId" claim in Azure B2C? Or should I create an "ObjectId" field in my database table that references the AD user? What would make more sense?
1a - I didn't add anything to the B2C tenant.
1b - I take the object id from B2C and store it in my table as an alternate key. My table has a unique id of it's own. Should I ever wish to have additional identity providers, this will be necessary.
I only use the object id from B2C to look up users and get my own id.
Question 2: Once a user registers, where and how would I update an AD User claim? Would I do it in one of these events? Or somewhere else? I see there is a "User is new" claim that I could check for?
When you say "update the claim" do you mean update permanently in the B2C tenant, or do you mean add it to the other claims and use it temporarily during the life of this particular token?
There's no connection back to B2C without using the graph client.
The userIsNew claim comes from B2C one time and only at the end of the signup process. You use that to determine if you have a new user trying to access your system. I hook that to create new entries in my tables from the claims that B2C gives me and from then on, the claims all come from the information in my tables.
Question 3: To update the claims, would I use: Microsoft.Azure.ActiveDirectory.GraphClient? Does anyone have any sample code for how to update a custom claim? I've tried this but it doesn't seem to persist:
I have to ask the "update" question again.
What you may be looking for is to "transform" the claims. That's usually done during a TicketReceived event for cookies. That occurs when they have authenticated for the first time. (Not to be confused with signing up.)
I'm not all that bright, but I'll tell you that I spent waaaay too much time on this trying to get it right. Mostly it's because there are a vast number of options and no one can tell you all the right ones for your project. So you just see reams of information that you have go through to find the bits you want.
I found this book (and it's author) incredibly helpful. It's current and he's a Microsoft guy who writes really well.
HTH
Regarding question 1: I did the same as nhwilly: I store the additional information in my database.
Regarding question 2: you can add claims in the OnsigningIn event:
app.UseCookieAuthentication(new Microsoft.AspNetCore.Builder.CookieAuthenticationOptions()
{
Events = new CookieAuthenticationEvents()
{
OnSigningIn = (context) =>
{
ClaimsIdentity identity = (ClaimsIdentity)context.Principal.Identity;
identity.AddClaim(new Claim("sb:tID", "555"));
return Task.FromResult(0);
}
}
});
I got the information from Transforming Open Id Connect claims in ASP.Net Core.
Regarding question 3: I haven't done it myself, but this link should get you kickstarted: https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-devquickstarts-graph-dotnet
Hope that helps!