We’re looking to query users in the azure ad b2c directory and programmatically extract their login email for all types of users. Which we understand to be:
- Standard User
- Guest User (B2B)
- Local User (B2C user who signed up with email)
- Social User (B2C user who signed up with a social account)
when running the api call using the azure ad graph explorer:
https://graph.windows.net/myorganization/users?api-version=1.6
we can see all these users. (data is sanitized a bit)
Example Standard User
{
"odata.type": "Microsoft.DirectoryServices.User",
"objectType": "User",
"objectId": "8b7c468b-fec4-4ff2-b448-64f99f3fa9ff",
"deletionTimestamp": null,
"accountEnabled": true,
"assignedLicenses": [],
"assignedPlans": [],
"city": null,
"companyName": null,
"country": null,
"creationType": null,
"department": null,
"dirSyncEnabled": null,
"displayName": "Global User",
"employeeId": null,
"facsimileTelephoneNumber": null,
"givenName": null,
"immutableId": null,
"isCompromised": null,
"jobTitle": null,
"lastDirSyncTime": null,
"mail": null,
"mailNickname": "global.user",
"mobile": null,
"onPremisesDistinguishedName": null,
"onPremisesSecurityIdentifier": null,
"otherMails": [],
"passwordPolicies": null,
"passwordProfile": null,
"physicalDeliveryOfficeName": null,
"postalCode": null,
"preferredLanguage": null,
"provisionedPlans": [],
"provisioningErrors": [],
"proxyAddresses": [],
"refreshTokensValidFromDateTime": "2017-10-31T17:20:29Z",
"showInAddressList": null,
"signInNames": [],
"sipProxyAddress": null,
"state": null,
"streetAddress": null,
"surname": null,
"telephoneNumber": null,
"usageLocation": null,
"userIdentities": [],
"userPrincipalName": "[email protected]",
"userType": "Member"
}
Example Guest User
{
"odata.type": "Microsoft.DirectoryServices.User",
"objectType": "User",
"objectId": "6458e1fc-c27b-40cb-b83d-2124f0999130",
"deletionTimestamp": null,
"accountEnabled": true,
"assignedLicenses": [],
"assignedPlans": [],
"city": null,
"companyName": null,
"country": null,
"creationType": null,
"department": null,
"dirSyncEnabled": null,
"displayName": "displayname",
"employeeId": null,
"facsimileTelephoneNumber": null,
"givenName": "givenname",
"immutableId": null,
"isCompromised": null,
"jobTitle": null,
"lastDirSyncTime": null,
"mail": null,
"mailNickname": "qa_theaccesshub.com#EXT#",
"mobile": null,
"onPremisesDistinguishedName": null,
"onPremisesSecurityIdentifier": null,
"otherMails": [
"[email protected]"
],
"passwordPolicies": null,
"passwordProfile": null,
"physicalDeliveryOfficeName": null,
"postalCode": null,
"preferredLanguage": null,
"provisionedPlans": [],
"provisioningErrors": [],
"proxyAddresses": [],
"refreshTokensValidFromDateTime": "2017-10-31T15:36:22Z",
"showInAddressList": null,
"signInNames": [],
"sipProxyAddress": null,
"state": null,
"streetAddress": null,
"surname": "surname",
"telephoneNumber": null,
"usageLocation": null,
"userIdentities": [],
"userPrincipalName": "qa_theaccesshub.com#EXT#@qa2clientb2ctheaccesshub.onmicrosoft.com",
"userType": "Member"
}
Example Local User
{
"odata.type": "Microsoft.DirectoryServices.User",
"objectType": "User",
"objectId": "a941e75d-2c1b-4383-9d6c-783c1d008479",
"deletionTimestamp": null,
"accountEnabled": true,
"assignedLicenses": [],
"assignedPlans": [],
"city": null,
"companyName": null,
"country": null,
"creationType": "LocalAccount",
"department": null,
"dirSyncEnabled": null,
"displayName": "Display Name",
"employeeId": null,
"facsimileTelephoneNumber": null,
"givenName": "Glen",
"immutableId": null,
"isCompromised": null,
"jobTitle": null,
"lastDirSyncTime": null,
"mail": null,
"mailNickname": "98c4f2cf-a452-46a4-a33f-6fb451bc3f59",
"mobile": null,
"onPremisesDistinguishedName": null,
"onPremisesSecurityIdentifier": null,
"otherMails": [],
"passwordPolicies": "DisablePasswordExpiration",
"passwordProfile": null,
"physicalDeliveryOfficeName": null,
"postalCode": null,
"preferredLanguage": null,
"provisionedPlans": [],
"provisioningErrors": [],
"proxyAddresses": [],
"refreshTokensValidFromDateTime": "2017-11-03T18:18:36Z",
"showInAddressList": null,
"signInNames": [
{
"type": "emailAddress",
"value": "[email protected]"
}
],
"sipProxyAddress": null,
"state": null,
"streetAddress": null,
"surname": "Martin",
"telephoneNumber": null,
"usageLocation": null,
"userIdentities": [],
"userPrincipalName": "98c4f2cf-a452-46a4-a33f-6fb451bc3f59@qa2clientb2ctheaccesshub.onmicrosoft.com",
"userType": "Member"
}
Example Social User
{
"odata.type": "Microsoft.DirectoryServices.User",
"objectType": "User",
"objectId": "917bddd5-40d8-4a25-9a6e-8317a6949b48",
"deletionTimestamp": null,
"accountEnabled": false,
"assignedLicenses": [],
"assignedPlans": [],
"city": null,
"companyName": null,
"country": null,
"creationType": null,
"department": null,
"dirSyncEnabled": null,
"displayName": "Display Name",
"employeeId": null,
"facsimileTelephoneNumber": null,
"givenName": "GivenName",
"immutableId": null,
"isCompromised": null,
"jobTitle": null,
"lastDirSyncTime": null,
"mail": null,
"mailNickname": "unknown",
"mobile": null,
"onPremisesDistinguishedName": null,
"onPremisesSecurityIdentifier": null,
"otherMails": [
"[email protected]"
],
"passwordPolicies": null,
"passwordProfile": {
"password": null,
"forceChangePasswordNextLogin": true,
"enforceChangePasswordPolicy": false
},
"physicalDeliveryOfficeName": null,
"postalCode": null,
"preferredLanguage": null,
"provisionedPlans": [],
"provisioningErrors": [],
"proxyAddresses": [],
"refreshTokensValidFromDateTime": "2017-11-02T13:48:09Z",
"showInAddressList": null,
"signInNames": [],
"sipProxyAddress": null,
"state": null,
"streetAddress": null,
"surname": "Surname",
"telephoneNumber": null,
"usageLocation": null,
"userIdentities": [],
"userPrincipalName": "cpim_662effe2-cd73-4f4a-8b42-2af5f68b2db1@qa2clientb2ctheaccesshub.onmicrosoft.com",
"userType": "Member"
}
In summary we notice:
Standard User
- the userPrincipalName looks normal
- the login email can be found at: userPrincipalName
- creation type is: null
Guest User
- the userPrincipalName looks to be the external directories upn followed by ‘#EXT#’ followed by this primary domain
- the login email can be found at: otherMails[0]
- creation type is: null
Local User
- the userPrincipalName looks to be some object id followed by this primary domain
- the login email can be found at: signInNames[0].value
- creation type is: LocalAccount
Social User
- the userPrincipalName looks to be ‘cpim_’ followed by some object id followed by this primary domain
- the login email can be found at: otherMails[0]
- creation type is: null
Although we can see some trends we’d rather not make guesses or bad assumptions. So we have the following questions:
1) What is the best way to know which of the four types of user we’re looking at?
2) Is there a simpler way to get the login (ideally as one field, we’re trying to build this into a simple mapping)? Other than :
If signInNames[0].value is not null use signInNames[0].value
Elseif otherMails[0] is not null use otherMails[0]
Else userPrincipalName
3) Do other social users behave differently? (We’ve only done Facebook so far.)
4) For social users what is the best way of knowing which identity provider is used?
UPDATE 11/8:
5) Why don’t LocalAccount users get an otherMail value?
6) Why don’t non-LocalAccount users get sign-in names? I guess that was added exclusively for B2C?
7) Also I guess end users don’t really have the ability to associate social and local accounts today? (unless using an app which leverages graph).
UPDATE 11/8 #2:
8) Is it safe (for now) to assume that I can disambiguate a B2C signed-up individual who can only login with a social account using the following criteria:
- creation type is: null – meaning they’re not a Local User
- userPrincipalName is prefixed with ‘cpim_’ followed by a UUID – meaning they are PROBABLY not a Standard or Guest user
Or would there be a better method?
2
Answers
1) These are not mutually exclusive properties. For example, a local account, guest or member user can also link their multiple social profiles to their account. The underlying data (available via Graph API) does not make any assumptions.
The most reliable way to know whether an account is a local user or not is to look at creationType property. You can also look at signInNames. Having said that, that would not imply that the user will not have their social profile linked in the future. It’s just that this functionality is not exposed in B2C standard policies.
2)
signInNames
is certainly used for signing in the user. However, a user can also have more than onesignInName
as far as the directory is concerned. That is whysignInNames
is a collection. So a utility company or bank may have an account id and email address both assignInNames
.otherMails
is not a property used to sign in a user. So you may want to skip that. Think ofotherMails
as an email address that is not used for any critical function by the directory (e.g. it’s not used for sign in or for password resets etc.)You would use
userPrincipalName
for work accounts.3) All social accounts are considered as external user identities, and are mapped in the same way. They are not yet available through Graph API, but when they are, they will appear as a collection too since a user may link to multiple social accounts.
4) Not possible today, but coming in the future.
UPDATE 11/8 to answer additional questions
5) Simply because there is no email address in B2C standard flow to add in
otherMails
for local account users. The email address they used to sign up is already insignInNames
property. Through Graph API, you could addotherMails
, and when a token is issued, it will show up in theemails
claim even for local account users.6) Non-local account users do not get a
signInName
because they do not need to sign in using asignInNames
. Work accounts use theuserPrincipalName
instead which can be tied to Office 365, Exchange, or on-premise AD. Social IDPs have external identities. So, there is no known scenario in whichsignInNames
needs to be used for non-local accounts at least today.7) Yes, it is not possible today to link a social account with a local account through standard policies, but can be done through custom policies. It is also not yet possible through Graph API (because external identities are not yet exposed), but will be possible in the future when the API is exposed.
Update 11/24:
8) If you look at user principal name, you will notice it is random. cpim_ can be removed any time, it is not part of the contract. In fact, IMHO, it should be removed already so apps do not take a dependency on it.
To determine this correctly, we are expecting to announce a new “userIdentities” property on the user account, using which you can determine which social IDPs is the user account linked to. That would be the best way to do this.
Also you can request information about identity providers via MSOL:
Output example
Unfortunately graph API does not contains field
AlternativeSecurityIds
with identity providers list.