Azure AD B2C Custom User Attributes
Asked Answered
P

1

6

I'm new to the Azure B2C world. I'm attempting to create a Custom User attribute to store data for our application. I've created it in the Azure portal and assigned it to my Signup/SignIn policy. However, I want to be able to update/read this value programtically. I've been going down the route of using Graph API and registering Extensions. So two questions:

1) Are extensions/custom attributes the same thing? 2) I've tried this code and the returned extensions are always empty:

 public void RegisterExtension()
    {
        string myRegisteredAppObjectId = "<>";
        string json = @"{
                        ""name"": ""My Custom Attribute"",
                        ""dataType"": ""String"",
                        ""targetObjects"": [
                            ""User""
                        ]
                        }";

        B2CGraphClient b2CGraphClient = new B2CGraphClient();
        b2CGraphClient.RegisterExtension(myRegisteredAppObjectId, json);
        var extensions = JsonConvert.DeserializeObject(b2CGraphClient.GetExtensions(myRegisteredAppObjectId).Result);

    }

B2CGraphClient.cs

 public class B2CGraphClient
{
    private string clientId { get; set; }
    private string clientSecret { get; set; }
    private string tenant { get; set; }

    private AuthenticationContext authContext;
    private ClientCredential credential;

    public B2CGraphClient(string clientId, string clientSecret, string tenant)
    {
        // The client_id, client_secret, and tenant are pulled in from the App.config file
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.tenant = tenant;

        // The AuthenticationContext is ADAL's primary class, in which you indicate the direcotry to use.
        this.authContext = new AuthenticationContext("https://login.microsoftonline.com/" + tenant);

        // The ClientCredential is where you pass in your client_id and client_secret, which are 
        // provided to Azure AD in order to receive an access_token using the app's identity.
        this.credential = new ClientCredential(clientId, clientSecret);
    }


    public async Task<string> DeleteUser(string objectId)
    {
        return await SendGraphDeleteRequest("/users/" + objectId);
    }

    public async Task<string> RegisterExtension(string objectId, string body)
    {
        return await SendGraphPostRequest("/applications/" + objectId + "/extensionProperties", body);
    }


    public async Task<string> GetExtensions(string appObjectId)
    {
        return await SendGraphGetRequest("/applications/" + appObjectId + "/extensionProperties", null);
    }


    private async Task<string> SendGraphPostRequest(string api, string json)
    {
        // NOTE: This client uses ADAL v2, not ADAL v4
        AuthenticationResult result = authContext.AcquireToken(Globals.aadGraphResourceId, credential);
        HttpClient http = new HttpClient();
        string url = Globals.aadGraphEndpoint + tenant + api + "?" + Globals.aadGraphVersion;

        Console.ForegroundColor = ConsoleColor.Cyan;
        Console.WriteLine("POST " + url);
        Console.WriteLine("Authorization: Bearer " + result.AccessToken.Substring(0, 80) + "...");
        Console.WriteLine("Content-Type: application/json");
        Console.WriteLine("");
        Console.WriteLine(json);
        Console.WriteLine("");

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

        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine((int)response.StatusCode + ": " + response.ReasonPhrase);
        Console.WriteLine("");

        return await response.Content.ReadAsStringAsync();
    }

    public async Task<string> SendGraphGetRequest(string api, string query)
    {
        // First, use ADAL to acquire a token using the app's identity (the credential)
        // The first parameter is the resource we want an access_token for; in this case, the Graph API.
        AuthenticationResult result = authContext.AcquireToken("https://graph.windows.net", credential);

        // For B2C user managment, be sure to use the 1.6 Graph API version.
        HttpClient http = new HttpClient();
        string url = "https://graph.windows.net/" + tenant + api + "?" + Globals.aadGraphVersion;
        if (!string.IsNullOrEmpty(query))
        {
            url += "&" + query;
        } 

        Console.ForegroundColor = ConsoleColor.Cyan;
        Console.WriteLine("GET " + url);
        Console.WriteLine("Authorization: Bearer " + result.AccessToken.Substring(0, 80) + "...");
        Console.WriteLine("");

        // Append the access token for the Graph API to the Authorization header of the request, using the Bearer scheme.
        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url);
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
        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));
        }

        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine((int)response.StatusCode + ": " + response.ReasonPhrase);
        Console.WriteLine("");

        return await response.Content.ReadAsStringAsync();
    } 
}

Of course, myRegisteredAppObjectId has a valid GUID in it.

Thanks

Puparium answered 2/8, 2018 at 15:43 Comment(0)
S
5

Are extensions/custom attributes the same thing?

Based on my test, extensions is the same thing as custom attributes.

I've tried this code and the returned extensions are always empty:

I add the custom propery MyCustomAttribute following this tutorial and use a custom attribute in my policy. You could refer to my test steps.

I download the B2C-GraphAPI-DotNet project from Github. Using following code to the custom attribute

var applications = client.GetApplications("$filter=startswith(displayName, 'b2c-extensions-app')").Result

var extension = client.GetExtensions(objectId).Result //objectId from the applications result.

Then we could get the custom properties from the extension.

enter image description here

Then you can then treat that attribute the same way you treat any other property on a user object

Such as create a user:

var jsonObject = new JObject
            {
                {"accountEnabled", true},
                {"country", "US"},
                {"creationType", "LocalAccount"},
                {"displayName", "Tomsun"},
                {"passwordPolicies", "DisablePasswordExpiration,DisableStrongPassword"},
                { "extension_42ba0de8530a4b5bbe6dad21fe6ef092_MyCustomAttribute","test2"},  //custom propery
                {"passwordProfile", new JObject
                {
                    {"password", "!QAZ1234wer"},
                    {"forceChangePasswordNextLogin", true}
                } },
                {"signInNames", new JArray
                    {
                        new JObject
                        {
                            {"value","[email protected]"},
                            {"type", "emailAddress"}
                        }
                    }
                }
            };

string user = client.CreateUser(jsonObject.ToString()).Result;

Query a user

var user = client.GetUserByObjectId(objectId).Result; //user objectId

enter image description here

Update a user

var jsonUpdate = new JObject
            {  
                { "extension_42ba0de8530a4b5bbe6dad21fe6ef092_MyCustomAttribute","testx"}

            };
var updateuser = client.UpdateUser("objectId", jsonObject2.ToString()).Result; //UserObject Id
Surfeit answered 3/8, 2018 at 5:47 Comment(3)
Thank you for this detailed answer Tom. I had a few things wrong in my code, including not removing the dashes from the ObjectId. Also, I didn't know that you had to set a value on the extension attribute for the user before it'd be returned back in a query. All in all, I got it working now. Thanks!Puparium
Did you find that the attributes were being cached by Azure B2C? After updating the attributes the user tries to do a silent refresh to obtain a new token with the new claims. However it takes about 30 seconds of repeatedly asking before we get those new claims.Fanchan
I have added one custom attribute in User flow. But i am unable to see this attribute in Users list. I want to update the attribute value.So any configuration needed to display custom attribute in User listPercentage

© 2022 - 2024 — McMap. All rights reserved.