getUser returns object with name undefined - azure-ad-b2c

I'm using MSAL to log into my B2C Active Directory
The login seems to work ok. I get an ID token, then can use that to get an access token.
If I then use the clientApplication to call getUser, it returns an object but the name is undefined. The user in Azure Active directory has the user.name field filled out
yield login();
yield put({ type: AUTH_BEGIN_GET_TOKEN });
const token = yield call(acquireTokenSilent);
yield put({ type: AUTH_SET_TOKEN, token });
console.log('getting user');
const user = ActiveDirectoryClient.clientApplication.getUser();
console.log('user', user);

I had forgotten to tick the display name in the application claim

The "Display Name" attribute is issued as the "name" claim in the Azure AD B2C token so this attribute must be selected in the "Application claims" settings for the Azure AD B2C policy.

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();

Reading Azure B2C custom attributes with Graph API

I have created an Azure B2C custom attribute called IsAdmin on the portal, added it to a Sign In / Sign Up user flow, and then using the Graph API, successfully created a new user with IsAdmin = true. If I then sign in using that new user I can see IsAdmin returned in the token as a claim. So far so good.
However I can't seem to see that custom attribute when querying via Graph API, nor can I search for it.
var user = await graphClient.Users["{GUID HERE}"]
.Request()
.GetResponseAsync();
The user is returned, but the custom attribute is not present.
var results = await graphClient.Users
.Request()
.Filter("extension_anotherguid_IsAdmin eq true")
.GetAsync();
Returns no results.
Does anyone have any idea?
When storing custom attribute in a B2C tenant, a microsoft's managed app registration is created :
Take the app id of this app registration, remove the dashes in the id and then use it like below :
import requests
# if your app registration b2c extensions app id id is aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee :
b2c-extensions-app-id-without-dashes="aaaaaaaabbbbccccddddeeeeeeeeeeee"
url = f"https://graph.microsoft.com/v1.0/users/?$select=extension_{b2c-extensions-app-id-without-dashes}_IsAdmin"
headers = {
'Content-type': 'application/json',
'Authorization': 'Bearer ' + msgraph_token
}
r = requests.request("GET", url, headers=headers)
Extensions are not returned by default. You need specify the extension in Select
var user = await graphClient.Users["{GUID HERE}"]
.Request()
.Select("extension_anotherguid_IsAdmin")
.GetResponseAsync();
The value should be available through AdditionalData.
var extValue = user.AdditionalData["extension_anotherguid_IsAdmin"];
Resources:
Extensibility

Unable to get username in Azure B2C after successful authentication

I have a React SPA that communicates with the backend API (Azure Function App). I've created an app registration for both the SPA and the Azure Function App following the steps outlined here. Both app registrations are hosted in a separate directory from the Azure Function app since I'm using AD B2C. I'm able to successfully authenticate the user and make requests to the backend. I'm using PKCE as the auth protocol and MSAL.js to manage the authentication flow.
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.
Here's what the login screen looks like:
Here's the relevant code from the SPA which handles auth:
const { instance, accounts, inProgress,} = useMsal();
if (accounts.length > 0) {
msalInstance
.acquireTokenSilent({
account: accounts[0],
scopes: [
"https://APP_URI/user_impersonation",
],
})
.then((token) => {
console.log("token res is", token);
console.log("access token is", token);
})
.catch((err) => {
console.log("err is", err);
});
Here's the return value from calling acquireTokenSilent:
{
"authority":"https://APP_NAMEb2c.b2clogin.com/APP_NAMEb2c.onmicrosoft.com/b2c_1_flow/",
"uniqueId":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"tenantId":"",
"scopes":[
"https://APP_NAMEb2c.onmicrosoft.com/reg_api/user_impersonation"
],
"account":{
"homeAccountId":"581776f4-6e16-454a-a6ae-ecb49f7f04aa-b2c_1_flow.07232d62-7285-4737-97eb-87f0f9b7c38e",
"environment":"APP_NAMEb2c.b2clogin.com",
"tenantId":"",
"username":"testUser#gmail.com",
"localAccountId":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"name":"unknown",
"idTokenClaims":{
"exp":1663191498,
"nbf":1663187898,
"ver":"1.0",
"iss":"https://APP_NAMEb2c.b2clogin.com/07232d62-7285-4737-97eb-87f0f9b7c38e/v2.0/",
"sub":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"aud":"473fe4d9-260b-46ad-9ad1-f4c4a4f211e6",
"nonce":"65c7ec69-2837-4bdf-b9e3-ae38dbb19c48",
"iat":1663187898,
"auth_time":1663187896,
"name":"unknown",
"emails":[
"testUser#gmail.com"
],
"tfp":"B2C_1_flow",
"at_hash":"qOHPceVj3fEhGGlRq6xh4g"
}
},
"idToken":"TD_TOKEN",
"idTokenClaims":{
"exp":1663191498,
"nbf":1663187898,
"ver":"1.0",
"iss":"https://APP_NAMEb2c.b2clogin.com/07232d62-7285-4737-97eb-87f0f9b7c38e/v2.0/",
"sub":"581776f4-6e16-454a-a6ae-ecb49f7f04aa",
"aud":"473fe4d9-260b-46ad-9ad1-f4c4a4f211e6",
"nonce":"65c7ec69-2837-4bdf-b9e3-ae38dbb19c48",
"iat":1663187898,
"auth_time":1663187896,
"name":"unknown",
"emails":[
"testUser#gmail.com"
],
"tfp":"B2C_1_flow",
"at_hash":"qOHPceVj3fEhGGlRq6xh4g"
},
"accessToken":"ACCESS_TOKEN",
"fromCache":true,
"expiresOn":"2022-09-14T21:38:18.000Z",
"correlationId":"9c71acbb-7ed4-4beb-a282-71ec7d924bd8",
"extExpiresOn":"2022-09-14T21:38:18.000Z",
"familyId":"",
"tokenType":"Bearer",
"state":"",
"cloudGraphHostName":"",
"msGraphHost":"",
"fromNativeBroker":false
}
As you can see, the username property has the emailAddress as it's value and not the actual username.
I've not been able to find concrete guidance on how to get the username. The one resource I found said that UserPrincipleName(UPN) is an optional claim and to add this value in the authToken I should add UPN as an optional claim in the token configuration tab, which is not available in B2C AD. I would love to get some guidance on what I'm doing wrong as getting the username should not be this hard, right ?
Edit 1: I can confirm that the username has been set; in the image below the username is denoted by User Principle Name:
The username property in MSAL's AccountInfo object is populated by the email claim in the ID token. The email claim will be an array, and if there are multiple emails, MSAL will only use the first one as username.
To receive UserPrincipleName (UPN) in the ID token, you'll need to set the user attributes in your B2C tenant. Unfortunately this doesn't seem to be possible with standard user-flows, so you'll need to build a custom policy and sign-in with that instead. See for more: User profile attributes

ServerError: AADSTS50058: A silent sign-in request was sent but none of the currently signed in user(s) match the requested login hint"

SSO fails "ServerError: AADSTS50058: A silent sign-in request was sent but none of the currently signed in user(s) match the requested login hint"
when I use same account for both work and personal azure account.
I have 2 AAD accounts (one is with my work account and the other one is personal account but both attached with same email and both are using same credentials). When I use msal.js library for single sign on application. It takes me to my work account where it asks me to validate the credentials (using standard pop up dialog) by giving full email address and does not authenticate properly even if give right credentials. As I need to login using my personal account
I expect this should validate using my ad alias#company.com credentials. I tried with different account option in the dialog, but it fails and shows up same full email account.
How can I use my adalias#company.com as a default user id?
Here are the piece of the code I am trying to use.
var msalConfig = {
auth: {
clientId: 'xxxxxxxxxx', // This is your client ID
authority: "https://login.microsoftonline.com/{tenantid}" // This is tenant info
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
var graphConfig = {
graphMeEndpoint: "https://graph.microsoft.com/v1.0/me"
};
var requestObj = {scopes: ["user.read", "email"]};
// Is there a way to change here to get the required user id?
var myMSALObj = new Msal.UserAgentApplication(msalConfig);
// Register Callbacks for redirect flow
myMSALObj.handleRedirectCallbacks(acquireTokenRedirectCallBack,
acquireTokenErrorRedirectCallBack);
myMSALObj.handleRedirectCallback(authRedirectCallBack);
function signIn() {
myMSALObj.loginRedirect(requestObj).then(function (loginResponse) {
// Successful login
acquireTokenPopupAndCallMSGraph();
}).catch(function (error) {
// Please check the console for errors
console.log(error);
});
}
Here is the error message I get:
ServerError: AADSTS50058: A silent sign-in request was sent but none of the
currently signed in user(s) match the requested login hint
The expected result is seamless login to other application.
If you want to provide a login_hint to indicate the user you are trying to authenticate try:
var requestObj = {scopes: ["user.read", "email"], loginHint: "adalias#company.com"};
Reference https://github.com/AzureAD/microsoft-authentication-library-for-js/wiki/FAQs#q10-how-to-pass-custom-state-parameter-value-in-msaljs-authentication-request-for-example-when-you-want-to-pass-the-page-the-user-is-on-or-custom-info-to-your-redirect-uri

In the groups claims of an AAD access token, why do I get a GUID that is not the objectId of any group in the AAD tenant?

Using a modified version of the Microsoft MSAL quickstart for node.js (original here), I successfully received an access token for the Azure Storage API using the implicit flow. The token included a groups claim, but one of the GUIDs in the claim does not seem to correlate with any group in the tenant. After removing the user from every group, the claim still contains that GUID (and as expected no others anymore):
"groups": [
"2cb3a5e8-4606-4407-9a97-616246393b5d"
],
A Google search for that GUID didn't result in any hits, so I'm assuming it is not a well-known GUID of some sort.
Why do I get this "unknown" GUID in a group claim?
The AAD tenant involved is a very small tenant, exclusively used by me for learning AAD and authentication. As such, it only contains a single group. The user involved is not a member of this single group.
I've looked at user page in the Azure Portal, which indeed shows that the user is "not a member of any groups". Azure CLI also show that the user isn't a member of any group:
$ az ad user get-member-groups --upn jurjen#stupendous.org
[]
$
The full list of groups in this tenant contains just a single group, and as you can see its ObjectID does not match the GUID I get in the claim:
$ az ad group list --query [].objectId --output tsv
b1cc46de-8ce9-4395-9c7c-e4e90b3c0036
$
I've also created another application registration and have it expose a dummy API. When using that dummy API as scope I again successfully receive
an access token, but this one again includes the same unknown GUID as the single group claim.
Here are the hopefully relevant bits of the code.
As mentioned above, first I retrieved an access token for Azure Storage:
var requestObj = {
scopes: ["https://storage.azure.com/user_impersonation"]
};
... but I get the exact same result with a dummy API:
var requestObj = {
scopes: ["api://7c7f72e9-d63e-44b6-badb-dd0e43df4cb1/user_impersonation"]
};
This bit logs the user in:
function signIn() {
myMSALObj.loginPopup(requestObj).then(function (loginResponse) {
//Successful login
showWelcomeMessage();
acquireTokenPopup();
}).catch(function (error) {
//Please check the console for errors
console.log(error);
});
}
The token is acquired here. I'm aware that callMSGraph won't work here given the scope of the token. I get the token from the browser console log and decode it using jwt.ms.
function acquireTokenPopup() {
//Always start with acquireTokenSilent to obtain a token in the signed in user from cache
myMSALObj.acquireTokenSilent(requestObj).then(function (tokenResponse) {
console.log("Access Token from cache: " + JSON.stringify(tokenResponse.accessToken));
callMSGraph(graphConfig.graphMeEndpoint, tokenResponse.accessToken, graphAPICallback);
}).catch(function (error) {
console.log(error);
// Upon acquireTokenSilent failure (due to consent or interaction or login required ONLY)
// Call acquireTokenPopup(popup window)
if (requiresInteraction(error.errorCode)) {
myMSALObj.acquireTokenPopup(requestObj).then(function (tokenResponse) {
console.log("Access Token after interaction: " + JSON.stringify(tokenResponse.accessToken));
callMSGraph(graphConfig.graphMeEndpoint, tokenResponse.accessToken, graphAPICallback);
}).catch(function (error) {
console.log(error);
});
}
});
}
You will also get directoryRole ids in the groups(got from the access token). You can request https://graph.microsoft.com/v1.0/me/memberOf to check the details. Here is the graph explorer.

Resources