Set display name / username for email local accounts in Azure AD B2C? - azure-ad-b2c

I'm new to Azure AD B2C and have set up a site that correctly authenticates using local accounts (email only). When the validation request comes back, I see the email address under the 'emails' claim, but the 'name' claim comes back as 'unknown'.
Looking in Azure portal, the account is created but the name is unset and is 'unknown' for all users that register. This isn't what I was expecting. I would prefer that the 'name' be set to the email address by default so that it is easier to find the account in the portal, since we aren't collecting a 'Display Name' at all for this account type (user already enters given and surname).
Do I have something configured incorrectly, and is there a way to default the username to the email address for local, email only accounts?

Azure AD B2C does not "auto-populate" any fields.
When you setup your sign-up policy or unified sign-up/sign-in policy you get to pick the Sign-up attributes. These are the attributes that are show to the user for him/her to provide and are then stored in Azure AD B2C.
Anything that the user is not prompted for is left empty or in a few select cases (like name as you have observed) set to 'unknown'.
Azure AD B2C can not make assumptions as to what to pre-populate a given attribute with. While you might find it acceptable to use the email as the default for the name, others might not. Another example, the display name, for some, can be prepopulated with "{Given name} {Surname}", but for others, it's the other way around "{Surname, Givenname}".
What you are effectively asking for is an easy way to configure defaults for some attributes which is not that's available today. You can request this feature in the Azure AD B2C UserVoice forum.
At this time, you have two options:
Force your users to explicitly provide this value by select it as a sign-up attribute in your policy.
Add some code that updates these attributes with whatever logic you want (for example in the controller that processes new sign-ups or via a headless client running periodically).
Here's a quick & dirty snippet of .Net code that you can use for this (assuming you want to do this in the auth pipeline (Startup.Auth.cs):
private async Task OnSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
try
{
var userObjectId = notification.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
// You'll need to register a separate app for this.
// This app will need APPLICATION (not Delegated) Directory.Read permissions
// Check out this link for more info:
// https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-devquickstarts-graph-dotnet
var authContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext(string.Format(graphAuthority, tenant));
var t = await authContext.AcquireTokenAsync(graphResource, new ClientCredential(graphClientId, graphClientSecret));
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + t.AccessToken);
var url = graphResource + tenant + "/users/" + userObjectId + "/?api-version=1.6";
var name = "myDisplayName";
var content = new StringContent("{ \"displayName\":\"" + name + "\" }", Encoding.UTF8, "application/json");
var result = await client.PostAsync(url, content);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
You'll reference this method when you setup your OpenIdConnectAuthenticationOptions like so:
new OpenIdConnectAuthenticationOptions
{
// (...)
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
SecurityTokenValidated = OnSecurityTokenValidated,
},
// (...)
};

I wrote this extension:
public static class ClaimsPrincipal
{
public static string Username(this System.Security.Claims.ClaimsPrincipal user)=> user.Claims.FirstOrDefault(c => c.Type == "preferred_username").Value;
}
Now you can use
User.Identity.Name for name if you have this in your OpenId config in the Startup.cs
options.TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "name" };
and User.Username if you include the extension

Related

GraphServiceClient returning user object with missing properties for Azure AD B2C User

I have a React SPA that communicates with my backend API (Azure Function App). I've created an app registration for both the SPA and the Azure Function App and I'm able to successfully authenticate the user and make requests to the backend. I'm using Azure AD B2C for IAM and I've configured a standard signup/signin policy for which I'm using Local Account as the Identity provider and username as the user id (see screenshot below for further context)
I'd like to fetch the username by calling the Graph API for the logged in user. For example, if the username is test123, I'd like to see this value represented in either the UserPrincipleName or the Identities property on the User object that's returned from the GraphServiceClient.
Here's the code that fetches the user object from MS GraphServiceClient:
var clientApp = ConfidentialClientApplicationBuilder
.Create(CLIENT_ID)
.WithTenantId(TENANT_ID)
.WithClientSecret(CLIENT_SECRET)
.Build();
var scopes = new string[] { "https://graph.microsoft.com/.default" };
GraphServiceClient graphServiceClient =
new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) => {
var authResult = await clientApp
.AcquireTokenForClient(scopes)
.ExecuteAsync();
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
})
);
var user = await graphServiceClient.Users[userObjectID]
.Request()
.GetAsync();
return new OkObjectResult(user);
Here is the truncated user object:
{
"displayName": "John Doe",
"userPrincipalName": "310c9d6b-7bc6-4052-894d-525b4a2e926f#APP_ID.onmicrosoft.com",
"identities": null,
"id": "310c9d6b-7bc6-4052-894d-525b4a2e926f",
"oDataType": "microsoft.graph.user",
"additionalData": {
"#odata.context": {
"valueKind": 3
}
}
}
The displayName is correct, but the value of userPrincipleName differs from what's shown in the user's profile on the B2C blade (as shown in the screenshot above). Furthermore, identities property, which I thought would contain the username value, is null.
The values for the clientApp come from an app registration that has the proper api permissions (see screenshot below).
Any help would be greatly appreciated. The bottom line is that I need to fetch the username value and from my research this should've been possible by fetching the user object from the graphAPI. Although I'm able to successfully fetch the user, the username value continues to evade me...
I can answer part of your questions. When we call ms graph api to get user information for a specific user, only default user properties will be returned if we don't use the select odata parameter. This is the reason why identities is null.
Try this code below, then you will find other properties become null
including displayName:
var user = await graphServiceClient.Users[userObjectID].Request().Select("identities").GetAsync();

How to access two separate Web APIs protected using Azure AD B2C from a web app

We have two separeate dotnet core apis(API1 & API2) that are protected using azure ad b2c. Both these apis are registered on the b2c tenant and have their scopes exposed.
We have a client web applicaiton that is to access the above protected apis. This web app has been registered as a applicaiton in b2c tenant and has api permissions set for the above apis with proper scopes defined.
We use MSAL.net with a signinpolicy to sign the user in to the web app.
the authentication call requires scopes to mentioned. So we add API1's scope in the call.
(note : one scope of a single resource can be added in a auth call shown below)
public void ConfigureAuth(IAppBuilder app)
{
// Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(Globals.WellKnownMetadata, Globals.Tenant, Globals.DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = Globals.ClientId,
RedirectUri = Globals.RedirectUri,
PostLogoutRedirectUri = Globals.RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claim type that specifies the Name property.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
ValidateIssuer = false
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {Globals.ReadTasksScope} {Globals.WriteTasksScope}",
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebCookieManager()
}
);
}
The OnAuthorizationCodeRecieved method in Startup.Auth.cs recieved the code recieved as a result of above auth call and uses it to get a access token based on the scopes provided and stores it in the cache. shown below
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
try
{
/*
The `MSALPerUserMemoryTokenCache` is created and hooked in the `UserTokenCache` used by `IConfidentialClientApplication`.
At this point, if you inspect `ClaimsPrinciple.Current` you will notice that the Identity is still unauthenticated and it has no claims,
but `MSALPerUserMemoryTokenCache` needs the claims to work properly. Because of this sync problem, we are using the constructor that
receives `ClaimsPrincipal` as argument and we are getting the claims from the object `AuthorizationCodeReceivedNotification context`.
This object contains the property `AuthenticationTicket.Identity`, which is a `ClaimsIdentity`, created from the token received from
Azure AD and has a full set of claims.
*/
IConfidentialClientApplication confidentialClient = MsalAppBuilder.BuildConfidentialClientApplication(new ClaimsPrincipal(notification.AuthenticationTicket.Identity));
// Upon successful sign in, get & cache a token using MSAL
AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(Globals.Scopes, notification.Code).ExecuteAsync();
}
catch (Exception ex)
{
throw new HttpResponseException(new HttpResponseMessage
{
StatusCode = HttpStatusCode.BadRequest,
ReasonPhrase = $"Unable to get authorization code {ex.Message}.".Replace("\n", "").Replace("\r", "")
});
}
}
This access token is then used in the TasksController to call AcquireTokenSilent which retrieves the access token from the cache, which is then used in the api call.
public async Task<ActionResult> Index()
{
try
{
// Retrieve the token with the specified scopes
var scope = new string[] { Globals.ReadTasksScope };
IConfidentialClientApplication cca = MsalAppBuilder.BuildConfidentialClientApplication();
var accounts = await cca.GetAccountsAsync();
AuthenticationResult result = await cca.AcquireTokenSilent(scope, accounts.FirstOrDefault()).ExecuteAsync();
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, apiEndpoint);
// Add token to the Authorization header and make the request
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
HttpResponseMessage response = await client.SendAsync(request);
// Handle the response
switch (response.StatusCode)
{
case HttpStatusCode.OK:
String responseString = await response.Content.ReadAsStringAsync();
JArray tasks = JArray.Parse(responseString);
ViewBag.Tasks = tasks;
return View();
case HttpStatusCode.Unauthorized:
return ErrorAction("Please sign in again. " + response.ReasonPhrase);
default:
return ErrorAction("Error. Status code = " + response.StatusCode + ": " + response.ReasonPhrase);
}
}
catch (MsalUiRequiredException ex)
{
/*
If the tokens have expired or become invalid for any reason, ask the user to sign in again.
Another cause of this exception is when you restart the app using InMemory cache.
It will get wiped out while the user will be authenticated still because of their cookies, requiring the TokenCache to be initialized again
through the sign in flow.
*/
return new RedirectResult("/Account/SignUpSignIn?redirectUrl=/Tasks");
}
catch (Exception ex)
{
return ErrorAction("Error reading to do list: " + ex.Message);
}
}
The issue is the code recieved by the OnAuthorizationCodeRecieved method can only be used to get the access token for API1 since its scope was mentioned in auth call. When trying to get access token for API2 it returns null.
Question : How to configure the web app so that it is able to access multiple protected apis?
Please suggest.
The code can be found from the sample https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi
A single access token can only contain scopes for a single audience.
You have 2 options:
Combine both services into a single app registration and expose different scopes.
Request multiple tokens - one per service. If your SSO policy is configured correctly in B2C, this should happen silently unbeknownst to the user.
I recommend using option 1 if you own both services (which it sounds like you do). A few tips related to this option.
When declaring the scopes in the combined app registration, use the dot-syntax {LogicalService}.{Operation}. If you do this, the scopes will be grouped by logical service within the Azure portal.
Make sure you are validating scopes in your service. Validating only the audience is not good enough and would allow an attacker to make lateral movements with a token bound for another service.

Get sensitivity labels error - Compliance policy not configured: missing <policy> tag

In Azure aip I have created an app, given all required permission and admin consent like described here in MIP sdk prerequisite section.
Here is the code as per the sdk to create the file engine.
public static IFileEngine CreateFileEngine(Identity identity)
{
// If the profile hasn't been created, do that first.
if (profile == null)
{
profile = CreateFileProfile(appInfo, ref authDelegate);
}
// Create file settings object. Passing in empty string for the first parameter, engine ID, will cause the SDK to generate a GUID.
// Locale settings are supported and should be provided based on the machine locale, particular for client applications.
var engineSettings = new FileEngineSettings("", "", "en-US")
{
// Provide the identity for service discovery.
Identity = identity
};
// Add the IFileEngine to the profile and return.
var engine = Task.Run(async () => await profile.AddEngineAsync(engineSettings)).Result;
return engine;
}
Here I am getting an exception saying "Compliance policy not configured" (See the image).
I have already checked in tenant AIP unified labelling and protection is activated and it's in sync with office 365 protection center's sensitivity labels.(Like recommended in this answer).
I am the global admin of the tenant and the token is generated using my credential.
public string AcquireToken(Identity identity, string authority, string resource, string claims)
{
AuthenticationContext authContext = new AuthenticationContext(authority);
string clientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"]?.ToString();
//var clientCred = new ClientCredential(AppInfo.ApplicationId, clientSecret);
var result = Task.Run(async () => await authContext.AcquireTokenAsync(resource, AppInfo.ApplicationId, new Uri(redirectUri), new PlatformParameters(PromptBehavior.Always))).Result;
//var result = authContext.AcquireTokenAsync(resource, clientCred).Result;
return result.AccessToken;
}
I have also 4/5 sensitivity labels published in aip.
What am I missing ?
You can either publish a policy in Office 365 Security and Compliance Centre to your users/groups, or copy your AIP policies to S&CC from the Azure portal (Azure Information Protection - Unified labeling / Copy policies (Preview). That should fix the issue.

MVC application Azure AD and custom authentication support

We have implemented Custom token based authentication in the MVC application. Now we enabled Azure AD as well using the OpenID Connect as described below.
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = ADClientId,
Authority = ADauthority,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (context) =>
{
if (context.Request.Path.Value == "/Account/ExternalLogin" || (context.Request.Path.Value == "/Account/LogOff"))
{
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl + "/";
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
}
else
{
context.State = Microsoft.Owin.Security.Notifications.NotificationResultState.Skipped;
context.HandleResponse();
}
return Task.FromResult(0);
},
}
We need to modify the scenario like below. If you have any technical suggestion let me know
1) Login page - get user email address
2) Check for user id and if that is Azure AD email - then take to the Microsoft authentication page where the user enters the password
3) If the user enters custom user id , handle the password page in the application's internal authentication flow
If your requirement is pivoted upon examining if a provided email address is from an existing user account in your Azure Active Directory tenant, then you can utilize the Microsoft Graph to query and confirm.
For example, the following Graph Api REST call would help determine if a provided email address is that of an existing user in your tenant.
https://graph.microsoft.com/v1.0/users?$filter=startswith(mail%2C+'email#domain.com')

Using the MVC Authorize attribute with roles using Azure Active Directory + OWIN

I'm building a multi-tenant MVC5 app that follows very closely the sample guidance: https://github.com/AzureADSamples/WebApp-MultiTenant-OpenIdConnect-DotNet/
I'm authenticating against Azure Active Directory and have my own role names that I inject as a Role claim during the SecurityTokenvalidated event:
SecurityTokenValidated = (context) =>
{
// retriever caller data from the incoming principal
string upn = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
string tenantId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
var databaseConnectionString = RoleEnvironment.GetConfigurationSettingValue("DatabaseConnectionString");
AppAnalyzerUser appAnalyzerUser = null;
using (CloudContext dbContext = new CloudContext(databaseConnectionString))
{
if (dbContext.Office365Accounts.FirstOrDefault(x => x.AzureTokenId == tenantId) == null)
throw new GeneralErrorException("Account not found", "The domain that you used to authenticate has not registered.");
appAnalyzerUser = (from au in dbContext.AppAnalyzerUsers
.Include(x => x.Roles)
where au.UserPrincipalName == upn && au.AzureTokenId == tenantId
select au).FirstOrDefault();
if (appAnalyzerUser == null)
throw new AccountNotFoundException();
}
foreach (var role in appAnalyzerUser.Roles)
{
Claim roleClaim = new Claim(ClaimTypes.Role, role.RoleName);
context.AuthenticationTicket.Identity.AddClaim(roleClaim);
}
return Task.FromResult(0);
},
I've decorated some methods with the Authorize attribute like this:
[Authorize(Roles = "SystemAdministrator"), HttpGet]
public ActionResult Index()
{
return View();
}
and the authorize attribute correctly detects that a user is not in that role and sends them back to Azure to authenticate.
However what I see is that the user is already authenticated against Azure AD and is logged in to the app. They don't get the chance to choose a new user account on the Azure screen to log in. So when it bounces them back to Azure AD, Azure AD says "you're already logged in" and sends them right back to the app. The SecurityTokenValidated event fires repeatedly, over and over.
But the user still doesn't have the role required for the method, so they get bounced back to Azure for authentication, and obviously we get stuck in a loop.
Other than writing my own implementation of the Authorize attribute, is there some other approach to solve this problem?
Unfortunately you stumbled on a known issue of [Authorize]. For a description and possible solutions see https://github.com/aspnet/Mvc/issues/634 - at this point writing a custom attribute is probably the most streamlined workaround.

Resources