Unable to get username in Azure B2C after successful authentication - azure

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

Related

Are the scopes defined when 'exposing an API' in an Azure AD Application Registration for information purposes only? If not, how are they enforced?

Desired Behaviour
I am setting up a Node.js web application using Azure AD B2C authentication and authorisation.
It is a confidential, server side, client (i.e. - not a Single Page Application).
The desired behaviour is:
Authenticate and authorise a user via login using Azure ADB2C
Based on a successful login, allow users to call routes in the Node web app
My specific question is at the bottom of this post, but is essentially:
Given that, if an accessToken is present in req.session, my
application will return the result from calling ANY endpoint I choose
(even if it is not related to any of the 'scopes' defined when
'exposing the api'), are the scopes that are defined when 'exposing an
API' essentially 'for information purposes only' - both for
application admins and end users? Or should they somehow be enforced in each relevant Express route handler?
Research
I have done extensive reading and video watching around the topic, including:
GitHub Repositories:
active-directory-b2c-msal-node-sign-in-sign-out-webapp
active-directory-b2c-javascript-nodejs-webapi
learn.microsoft.com articles:
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-overview
https://learn.microsoft.com/en-au/azure/active-directory-b2c/configure-a-sample-node-web-app
https://docs.microsoft.com/azure/active-directory-b2c/enable-authentication-in-node-web-app
https://docs.microsoft.com/azure/active-directory-b2c/configure-authentication-in-sample-node-web-app-with-api
https://docs.microsoft.com/azure/active-directory-b2c/enable-authentication-in-node-web-app-with-api
Videos:
Identity for Developers Playlist by Microsoft Security
OAuth 2.0 and OpenID Connect (in plain English) by OktaDev
What I've Tried
I have been able to implement a basic prototype consisting of:
Node.js web application (a confidential, server side, client, not a Single Page App)
Sign Up, Sign In and Edit Profile User flows
2 x Application Registrations in Azure - one for the Web App and one for the Web App API
I have 'exposed the API' in the Web App API registration, eg: :
https://my-tenant-name.onmicrosoft.com/my-api-uri-thing/tasks.read
https://my-tenant-name.onmicrosoft.com/my-api-uri-thing/tasks.write
I have 'added permissions' to the Web App registration (to access the Web App API registration), eg:
MY-APP-API (2)
tasks.read, Delegated, Admin Consent Required - Yes, Granted for MY-APP
tasks.write, Delegated, Admin Consent Required - Yes, Granted for MY-APP
Microsoft Graph (2)
offline_access, Delegated, Admin Consent Required - No, Granted for MY-APP
openid, Delegated, Admin Consent Required - No, Granted for MY-APP
So the workflow is as follows (based on this code):
User clicks on a '/signin' link
The relevant Express route handler passes through the required scopes to getAuthCode():
app.get('/signin',(req, res)=>{
//Initiate a Auth Code Flow >> for sign in
//Pass the api scopes as well so that you received both the IdToken and accessToken
getAuthCode(process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY,apiConfig.webApiScopes, APP_STATES.LOGIN, res);
});
Source
The value of the scopes parameter is:
['https://${process.env.TENANT_NAME}.onmicrosoft.com/my-api-uri-thing/tasks.read','https://${process.env.TENANT_NAME}.onmicrosoft.com/my-api-uri-thing/tasks.write']
Upon successful sign in, the user is redirected to /redirect
The relevant Express route handler passes through a tokenRequest object to get an accessToken which is then added to req.session:
app.get('/redirect',(req, res)=>{
if (req.query.state === APP_STATES.LOGIN) {
// prepare the request for calling the web API
tokenRequest.authority = process.env.SIGN_UP_SIGN_IN_POLICY_AUTHORITY;
tokenRequest.scopes = apiConfig.webApiScopes;
tokenRequest.code = req.query.code;
confidentialClientApplication.acquireTokenByCode(tokenRequest)
.then((response) => {
req.session.accessToken = response.accessToken;
req.session.givenName = response.idTokenClaims.given_name;
console.log('\nAccessToken:' + req.session.accessToken);
res.render('signin', {showSignInButton: false, givenName: response.idTokenClaims.given_name});
}).catch((error) => {
console.log(error);
res.status(500).send(error);
});
}else{
res.status(500).send('We do not recognize this response!');
}
});
User calls a 'protected API'
The relevant Express route handler checks if req.session contains an accessToken value
If the accessToken is present, it makes an http request to the desired endpoint using axios and passes through the accessToken as the Bearer token in the headers of the request.
The 'protected content' is then returned
app.get('/api', async (req, res) => {
if(!req.session.accessToken){
//User is not logged in and so they can only call the anonymous API
try {
const response = await axios.get(apiConfig.anonymousUri);
console.log('API response' + response.data);
res.render('api',{data: JSON.stringify(response.data), showSignInButton: true, bg_color:'warning'});
} catch (error) {
console.error(error);
res.status(500).send(error);
}
}else{
//Users have the accessToken because they signed in and the accessToken is still in the session
console.log('\nAccessToken:' + req.session.accessToken);
let accessToken = req.session.accessToken;
const options = {
headers: {
//accessToken used as bearer token to call a protected API
Authorization: `Bearer ${accessToken}`
}
};
try {
const response = await axios.get(apiConfig.protectedUri, options);
console.log('API response' + response.data);
res.render('api',{data: JSON.stringify(response.data), showSignInButton: false, bg_color:'success', givenName: req.session.givenName});
} catch (error) {
console.error(error);
res.status(500).send(error);
}
}
});
Question
Given that, if an accessToken is present in req.session, my application will return the result from calling ANY endpoint I choose (even if it is not related to any of the 'scopes' defined when 'exposing the api'), are the scopes that are defined when 'exposing an API' essentially 'for information purposes only' - both for application admins and end users?
Or should I be coding in a conditional statement in each relevant Express route handler that says:
IF the required scope for accessing this content is present in this access token,
THEN you can access this content
and therefore 'enforces' the scope that has been defined and consented to by the user.
With my current level of understanding, I am assuming that these scopes ARE just for information purposes only, because I haven't seen any examples where the scopes are enforced through code.

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

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.

getUser returns object with name undefined

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.

Resources