0

I would like to allow a user to change their password in my app (not reset, actually change) by supplying the current password and new password.

Looking at the documentation I tried it, and the call doesn't fail, but then neither the old nor the new password end up working and I had to delete the user. For reference, I'll post my code:

    var graphClient = GetGraphServiceClient();

    var identity = new ObjectIdentity
    {
        Issuer = Globals.Tenant,
        IssuerAssignedId = userEmail,
        SignInType = "emailAddress"
    };

    var user = new User
    {
        PasswordProfile = new PasswordProfile
        {
            Password = newPassword
        },
        Identities = new ObjectIdentity[] { identity }
    };

    await graphClient.Users[userId]
        .Request()
        .UpdateAsync(user);

I surmised from documentation and looking around that this is not the correct way to do this as I would need to do it on behalf of the signed in user, and not the application. However, for AD B2C, under portal, I can't grant delegate permissions other than open_id or offline_access to my registered app.

After coming across this, I figured that I need to actually try this call with signed-in user's access token, and not the application's access token. So I assign the user's token code to a claim so I can access it later:

//in startup.auth.cs
 private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
        {
            try
            {
                notification.AuthenticationTicket.Identity.AddClaim(new Claim("access_token", notification.Code));

...

//in changePassword() method:
            //retrieve user access code from the claim stored previously when logging them in.
            var userToken = ClaimsPrincipal.Current.FindFirst("access_token").Value;

            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri(Globals.GraphApiEndpoint);
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", userToken);
                try
                {
                    JObject o = JObject.FromObject(new
                    {
                        currentPassword,
                        newPassword
                    });
                    await client.PostAsJsonAsync($"v1.0/me/changePassword/", o.ToString());

                }
                catch(Exception ex)
                {
                    //never any exception. always succeeds!
                }
            }

The above always succeeds no matter what passwords I pass in. But the password obviously doesn't change at all. What am I doing wrong? Is there another way to do this?

Edit: seems like my first approach works if I do the following:

            var user = new User
            {
                PasswordProfile = new PasswordProfile
                {
                    Password = newPassword,
                    ForceChangePasswordNextSignIn = false
                },
                Identities = new ObjectIdentity[] { identity }
            };

            var userInfo = await graphClient.Users[userId]
                .Request()
                .UpdateAsync(user);

However, the issue now is that I can't first validate this using the user's old password, since it only takes in a new password and updates it. Any ideas?

Edit 2: after trying via postman and getting "x5t is invalid" when trying to use the second approach of fetching the user token and request it at "v1.0/me/changePassword/" endpoint, I was able to find this response here:

"Unfortunately, B2C tenant doesn't support this method to reset the password as there is no UserAuthenticationMethod.ReadWrite.all permission included in Graph API for B2C tenant. The only delegated permissions available in B2C tenant are offline_access and openID.The methods available to reset the password in B2C tenant are either admin performing password reset via Azure Portal or by using Password Reset Policy.

Password Reset via Graph API is only supported in Standard Azure AD tenants as of now. You can post an idea at our Feedback Portal regarding this feature in B2C tenant."

So I am guessing that the fact that I was somehow able to use graph api to change the password profile above is not supposed to happen?

Riz
  • 6,486
  • 19
  • 66
  • 106
  • What about a change password flow: https://learn.microsoft.com/en-us/azure/active-directory-b2c/add-password-change-policy?pivots=b2c-custom-policy – Jas Suri - MSFT Feb 26 '21 at 22:22
  • 2
    Thanks @JasSuri-MSFT. That's the conclusion I have come to. I just updated my post. It's just frustrating that I had to spend so much time doing this rather than it being clearly pointed out in the documentation. AD B2C team really really needs to improve their documentation. Every single thing I have tried to do so far has been a struggle because of confusing or missing documentation. – Riz Feb 26 '21 at 22:27
  • What you learnt is not unexpected. AAD B2C contains both the B2C and AAD functionality depending on which endpoint you use. See an explanation here https://stackoverflow.com/a/62693315/8357357 – Jas Suri - MSFT Feb 26 '21 at 22:34
  • 2
    I'm not sure why it would be not unexpected. I would expect a product like this to have consistency, not special cases all over the place. – Riz Feb 26 '21 at 22:58

2 Answers2

0

The only delegated permissions available in B2C tenant are offline_access and openID.

It's not true.

To call Microsoft Graph API, you have to use AAD auth flow rather than AAD B2C auth flow. So you don't need to use the same AAD application (app registration) as the one your b2c user flow uses.

You can create a new AAD app with the type Accounts in this organizational directory only (your b2c tenant only - Single tenant) or Accounts in any organizational directory (Any Azure AD directory – Multitenant).

Then you could assign Microsoft Graph permission UserAuthenticationMethod.ReadWrite.all into it and use this new AAD app to generate the access token to call Microsoft Graph.

AAD app with type Accounts in any identity provider or organizational directory (for authenticating users with user flows) can not be added Microsoft delegated permission.

We can successfully call POST https://graph.microsoft.com/v1.0/me/changePassword in Microsoft Graph Explorer to change password for B2C users, which also proves that Microsoft Graph indeed work for B2C users.

In the same way, it is also feasible to use Update user to change the password.

However, the issue now is that I can't first validate this using the user's old password, since it only takes in a new password and updates it. Any ideas?

I don't quite understand what can't first validate this using the user's old password means. Please provide more details about this question if you need to investigate this direction.

Allen Wu
  • 15,529
  • 1
  • 9
  • 20
  • I was able to do this via the policy route - which allows me to confirm the old password and then supply a new password as is standard practice. That's what I meant by "validating the user's old password." – Riz Mar 01 '21 at 06:00
  • I understand that we can do this kind of thing with AD instead of AD B2C auth flow now, but my point still stands as it says right there in B2C portal as well as according to your developers that AD B2C only allows open_id and offline delegated permissions. The link I provided originally is pointing to microsoft forums responded to by a microsoft developer which states that exact same thing. In case you missed it, here it is again: https://learn.microsoft.com/en-us/answers/questions/246207/34upn-from-claims-with-value-null-is-not-a-valid-u.html – Riz Mar 01 '21 at 06:04
  • As for the information you just provided, you must admit that it is quite confusing and also not clearly stated that I must use AAD instead of AD B2C auth flow in order to use Microsoft Graph API. If I am not supposed to use Graph API with AD B2C, then why does your documentation claim that I can? https://learn.microsoft.com/en-us/azure/active-directory-b2c/microsoft-graph-operations – Riz Mar 01 '21 at 06:07
  • 1
    @Riz I didn't miss it. Can you just try my suggestion? – Allen Wu Mar 01 '21 at 06:07
  • @Riz Please recheck this link: https://learn.microsoft.com/en-us/azure/active-directory-b2c/microsoft-graph-get-started?tabs=app-reg-ga. It is included in the link you provided:https://learn.microsoft.com/en-us/azure/active-directory-b2c/microsoft-graph-operations#prerequisites. Then in this part: https://learn.microsoft.com/en-us/azure/active-directory-b2c/microsoft-graph-get-started?tabs=app-reg-ga#register-management-application. **6.Select Accounts in this organizational directory only**. – Allen Wu Mar 01 '21 at 06:09
  • I don't need to anymore, as I stated earlier that I went the policy route and was able to accomplish this. It's just frustrating that I couldn't get clear answers from your documentation. – Riz Mar 01 '21 at 06:09
  • @Riz OK. Glad to know you have made it work using your own method. You can post an answer for others' reference in this case. – Allen Wu Mar 01 '21 at 06:12
  • @Riz can you post your answer on how to change user password via Policy (with validation of an old password, of course)? I am stack on the same problem. – Sebastian Budka May 10 '21 at 10:13
  • 2
    hi @SebastianBudka, I had already posted an answer with this link: https://learn.microsoft.com/en-us/azure/active-directory-b2c/add-password-change-policy?pivots=b2c-custom-policy But apparently Machavity had needlessly deleted it. Hopefully it helps you. – Riz May 11 '21 at 11:37
0

This simple code should work:

  var currentUser = users.Where(x => x.Mail == accountEmail).ToArray();

        if (currentUser.Length!=0)
        {
            Console.WriteLine(currentUser[0].Id);
            try
            {
                var user = new User
                {
                    PasswordProfile = new PasswordProfile
                    {
                        ForceChangePasswordNextSignIn = true,
                        Password = GeneratedPassword
                    }

                };

                

              await graphServiceClient.Users[currentUser[0].Id]
                    .Request()
                    .UpdateAsync(user);

                EmailService.SendEmail(currentUser[0].GivenName, "Your password has been reseted", ConfigurationManager.AppSettings["url"], GeneratedPassword, currentUser[0].Mail);

            }
            catch(Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
       
MJ X
  • 8,506
  • 12
  • 74
  • 99