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
Related
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();
I'm struggling a bit with Microsoft authentication architecture to access resources using the Graph API.
Let me explain my use-case: I have an Outlook account, which I need to insert events into the Calendar. I also have a REST API, in Node.js, that should read these events, using /me/events or /users/{id}/events Graph endpoint.
Since it is only one user, I don't need to implement login, but rather have the REST API be able to get an Authorization token to access these resources.
I tried to use the ConfidentialClientApplication class to login using the client_id and client_secret for my application (configured through Azure), but whenever I call the Microsoft Graph after login, I receive a 401.
Assuming that the problem is that the login I'm performing is with an admin account, I added the Application type Calendars.Read permission, to no help.
What am I doing wrong?
I just need to access this users' Calendar :(
Thanks for making it this far!
If you want to use ConfidentialClientApplication to get an access token and call Microsoft Graph API to read users' Calendar, try the code below :
const msal = require('#azure/msal-node');
const fetch = require('node-fetch');
const tenant= '<your tenant ID/name>'
const appID= '<azure ad app id>'
const appSec = '<azure ad app sec>'
const userID = '<user ID/UPN>'
const config = {
auth: {
clientId: appID,
authority: "https://login.microsoftonline.com/" + tenant,
clientSecret: appSec
}
}
function readUserCalendar(userID,accessToken){
const URL = 'https://graph.microsoft.com/v1.0/users/'+userID+'/events?$select=subject'
fetch(URL,{headers: { 'Authorization': 'Bearer ' + accessToken}})
.then(res => res.json())
.then(json => console.log(json));
}
const pca = new msal.ConfidentialClientApplication(config);
const acquireAccessToken = pca.acquireTokenByClientCredential({
scopes: ["https://graph.microsoft.com/.default"]
});
acquireAccessToken.then(result=>{readUserCalendar(userID,result.accessToken)})
Permissions grand to azure ad app in this case :
My test account calendar data:
Result :
I have a test user ID as test#gollahalliauth.onmicrosoft.com (without global admin rights) and I am trying to access Graph API for Azure AD.
Try 1 (Success)
I used Azure AD Graph Explorer, logged in with test#gollahalliauth.onmicrosoft.com and using the API https://graph.windows.net/gollahalliauth.onmicrosoft.com/users/test#gollahalliauth.onmicrosoft.com to get the contents. I was able to do this without any issue.
Try 2 (Fail)
I wrote a Go program with profile edit policy
import (
"crypto/rand"
"encoding/base64"
"fmt"
"golang.org/x/oauth2"
"os"
)
const AuthDomainName string = "https://gollahalliauth.b2clogin.com/gollahalliauth.onmicrosoft.com/oauth2/v2.0"
func main() {
conf := &oauth2.Config{
ClientID: os.Getenv("clientID"),
ClientSecret: os.Getenv("clientSecret"),
RedirectURL: "http://localhost:8080/callback",
Scopes: append([]string{"openid", "profile"}),
Endpoint: oauth2.Endpoint{
AuthURL: AuthDomainName + "/authorize?p=b2c_1_gollahalli_edit",
TokenURL: AuthDomainName + "/token?p=b2c_1_gollahalli_edit",
},
}
// Generate random state
b := make([]byte, 32)
rand.Read(b)
state := base64.StdEncoding.EncodeToString(b)
parms := oauth2.SetAuthURLParam("response_type", "id_token")
url := conf.AuthCodeURL(state, parms)
fmt.Println("AUth URL:",url)
}
This creates an auth URL to get the token. I used the id_token to access the graph API using Authorization: Barer id_token and I get an error as
{
"odata.error": {
"code": "Authentication_ExpiredToken",
"message": {
"lang": "en",
"value": "Your access token has expired. Please renew it before submitting the request."
}
}
}
Try 3 (Fail)
I tried adding User.Read in Azure AD B2C > Applications >
<application name> > Published scopes and used the full scope URL and now I get an error as Error: AADB2C90205: This application does not have sufficient permissions against this web resource to perform the operation.
I am not sure what the problem is here. Any idea as to how to get over this?
The AAD B2C is a specialized instance of AAD. You may consider it as a AAD tenant with some B2C extensions. Note: this is a separate tenant from your organization's main AAD tenant in which you've already created the B2C directory/feature!
You can access the B2C records through the AAD Graph API, in 2 steps:
acquire an AAD Graph token by providing the ClientID and ClientSecret to the AAD endpoint (e.g. https://login.microsoftonline.com/yourtenant.onmicrosoft.com).
connect to the AAD Graph REST endpoint (e.g. https://graph.windows.net/yourtenant.onmicrosoft.com/users?api-version=1.6) with the desired method (GET/POST/PATCH/DELETE), passing it the token obtained in step 1 in the Authentication header of the request.
The best example is probably the user migration tool provided by MS. The AAD B2C configuration is covered here and the sample code can be downloaded from the documentation page or directly from the Github project.
You should take a look to the SendGraphPostRequest method and its friends in B2CGraphClient.cs. The code uses ADAL to get the AAD Graph token, but you can also obtain it directly with REST requests. A simplified version in C# (you'll have to translate it yourself to GO, and maybe replace ADAL if it's not available in GO):
// NOTE: This client uses ADAL v2, not ADAL v4
AuthenticationResult result = aadAuthContext.AcquireToken(Globals.aadGraphResourceId, aadCredential);
HttpClient http = new HttpClient();
string url = Globals.aadGraphEndpoint + tenant + api + "?" + Globals.aadGraphVersion;
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
HttpResponseMessage response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
string error = await response.Content.ReadAsStringAsync();
object formatted = JsonConvert.DeserializeObject(error);
throw new WebException("Error Calling the Graph API: \n" + JsonConvert.SerializeObject(formatted, Formatting.Indented));
}
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
I've been trying (and failing) to create an Azure Stream Analytics job programatically. I was following this example originally:
https://azure.microsoft.com/en-gb/documentation/articles/stream-analytics-dotnet-management-sdk/
But it pops up a dialog for you to log in. I want to be able to do this server side. It looks like I need to use Azure AD to use the Resource Manager APIs. I've been working my way through this:
https://msdn.microsoft.com/en-us/library/azure/dn790557.aspx#bk_portal
And the code looks like this:
var authContext = new AuthenticationContext("https://login.microsoftonline.com/{tenant id}/oauth2/token");
var clientId = "{app client id}";
var appKey = "{app key}";
var subscriptionId = "{subscription id}";
var clientCredential = new ClientCredential(clientId, appKey);
var result = authContext.AcquireToken("https://management.core.windows.net/", clientCredential);
var creds = new TokenCloudCredentials(subscriptionId, result.AccessToken);
var client = new StreamAnalyticsManagementClient(creds);
var jobCreateParameters = new JobCreateOrUpdateParameters
{
Job = new Job
{
Name = streamAnalyticsJobName,
Location = "North Europe",
Properties = new JobProperties
{
EventsOutOfOrderPolicy = EventsOutOfOrderPolicy.Adjust,
Sku = new Sku
{
Name = "Standard"
}
}
}
};
var jobCreateResponse = client.StreamingJobs.CreateOrUpdate(resourceGroupName, jobCreateParameters);
I can successfully acquire a token, but creating the job fails:
AuthorizationFailed: The client 'REDACTED' with object id 'REDACTED' does not have authorization to perform action 'Microsoft.StreamAnalytics/streamingjobs/write' over scope '/subscriptions/REDACTED/resourcegroups/REDACTED/providers/Microsoft.StreamAnalytics/streamingjobs/REDACTED'
Am I doing something wrong? The app has the delegated permissions set.
UPDATE - 08-Dec-2015
There's an easy way to assign roles to Service Principals now. Please see this link for more details: https://azure.microsoft.com/en-in/documentation/articles/resource-group-create-service-principal-portal/.
Original Response
When you grant access to an application to your Azure subscription, behind the scenes a user is created with Service Principal user type in your Azure AD. The code you're using below assumes that you're using this Service Principal user when getting access token.
var clientCredential = new ClientCredential(clientId, appKey);
var result = authContext.AcquireToken("https://management.core.windows.net/", clientCredential);
var creds = new TokenCloudCredentials(subscriptionId, result.AccessToken);
However by default this user is not granted any permissions (RBAC) on your subscription and that's why you're getting the authorization error.
To solve this problem, what you would need to do is grant appropriate role to this user in your subscription. Now you can use PowerShell to do so or you can do it via code using ADAL library + making some web requests.
What I did was I made use of ADAL library to get access tokens and then used Google Postman (or Fiddler) to do other stuff. In my case, it was a web application. Here's what I did:
I logged in into the application as Global Administrator (Subscription Owner) and got a code. Using that code and ADAL library, I got the access token (let's call it token1).
var authContext = new AuthenticationContext(string.Format("{0}/common", signinEndpoint));//signinEndpoint = https://login.windows.net
var result = await authContext.AcquireTokenByAuthorizationCodeAsync(code, redirectUri, credential);
I copied the tenant id and access token returned to me in result above.
Next thing I did was I found out the object id of the Service Principal user using POSTMAN. This is the GET URL I executed there. For Authorization header, you would need to use Bearer {token1}.
https://graph.windows.net/{subscription-id}/servicePrincipals?api-version=1.5&$filter=appId eq '{app-client-id}'
After that I acquired another access token (let's call it token2) for Service Management API operation using the code below:
authContext = new AuthenticationContext(string.Format("{0}/{1}", signinEndpoint, result.TenantId));
result = await authContext.AcquireTokenSilentAsync(serviceManagementApiEndpoint, credential, new UserIdentifier(request.UserInfo.UniqueId, UserIdentifierType.UniqueId));//serviceManagementApiEndpoint = https://management.core.windows.net/
After that I listed the roles in my subscription and picked the role I wanted to assign to my Service Principal user. In my case, I wanted to assign a Reader role so I noted down the role's id. For Authorization header, you would need to use Bearer {token2}.
https://management.azure.com/subscriptions/{subscription-id}/providers/Microsoft.Authorization/roleDefinitions?api-version=2015-06-01
Next is assignment of this role to the user. For this I created a guid as role assignment id and used the following URL:
https://management.azure.com/subscriptions/{subscription-id}/providers/microsoft.authorization/roleassignments/{role-assignment-id}?api-version=2015-06-01
It's going to be a PUT request and this was the request body would be something like:
{
"properties":
{
"roleDefinitionId": "{role id of the selected role from step 5}",
"principalId": "{id of the service principal from step 3"
}
}
Please ensure that the content-type of the request is set as application/json;odata=verbose and not application/json.
That's pretty much it! After that your code should work just fine :)
Give it a try and see what happens.