Azure-Samples / active-directory-b2c-dotnet-webapp-and-webapi

A combined sample for a .NET web application that calls a .NET web API, both secured using Azure AD B2C
http://aka.ms/aadb2c
MIT License
274 stars 236 forks source link

Azure AD B2C - .NET-Web app calling web api - No account or login hint was passed to the AcquireTokenSilent call #149

Open spalmcc opened 1 year ago

spalmcc commented 1 year ago

Hello All,

We have configured web app and web api on Azure as per the sample code and instructions provided by Micosoft (https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi)

What is the issue? When I am trying to access the token in web controller, I am getting the following error: "No account or login hint was passed to the AcquireTokenSilent call."

Refer the following image where we can making a call to get the token. image

Refer the following image where the method details are there. image

The main issue is, account is coming as null. Refer the following image:- image

We are not able to find where is the issue, why account is coming as null.

So I did the following to ensure web api is configured properly:-

Added one more redirect uri on Web Api on Azure (https://www.postman.com/oauth2/callback) for postman Added all configuration in postman Started the web api project locally Manage to get access token Able to hit one end point successfully to the web api(Get call) Able one end point successfully to the web api (Post call) So this test ensures that web api project is configured in Azure and working fine using Azure AD B2C Authentication.

I did a similar test by running a userflow againts the web api project and I could see tokens getting generated (Not sure if this is a legitimate test)

I have been reading the issue on internet.But not able to find an exact issue like this. Any pointers would be very hekpfull. best regards

BrianS-CF commented 1 year ago

Logged exactly the same ticket with MSFT back in Jan. They were unable/unprepared to provide support for the sample code despite it being from Microsoft.

regards

spalmcc commented 1 year ago

@BrianS-CF thanks for sharing this with me. This is very strange that Microsoft team is not at all responding. I am sort of stuck with this issue. And there is no way, I can reach out to any of the team mates who worked on MSAL.NET library.

bgavrilMS commented 1 year ago

We will revamp this sample to use Microsoft.Identity.Web - our higher level API https://github.com/AzureAD/microsoft-identity-web/wiki/asp-net

If you use ASP.NET Core (all new projects should!), then I recommend looking at the official ASP.NET Core sample:

https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master

spalmcc commented 1 year ago

Hello bgavrilMS, we are not using .NET Core, our app is based on .NET (4.7.2 && MVC 3). We are struggling to know why we are getting account as null.

Any pointers for troubleshotting would be very helpful.

bgavrilMS commented 1 year ago

@spalmcc - when you log in the user the first time, you get an auth code and that gets exchanged for a token. This happens here: https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi/blob/d2a78d9a7a4e66547314bf58ef526932af5d8077/TaskWebApp/App_Start/Startup.Auth.cs#L133

Look at AuthenticationResult property - it will have an Account object - note its id. This is when the auth artefacts get written to the cache (i.e. cache SET operation)

Next, look at the GetB2CMsalAccountIdentifier / GetAccountsCall. What account id does it compute. What is the value here? (this is the cache GET operation).

Note: I think I can alraedy spot a problem - https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi/blob/d2a78d9a7a4e66547314bf58ef526932af5d8077/TaskWebApp/Utils/ClaimsPrincipalExtension.cs#L50 - it hardcodes the SingUpSingIn policy. As far as I know, with B2C, there is a different "account" for each policy. So this helper needs to be updated for other policies.

bgavrilMS commented 1 year ago

CC @jmprieur

spalmcc commented 1 year ago

@bgavrilMS Please have a look to the following:-

private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification) { try { / The MSALPerUserMemoryTokenCache is created and hooked in the UserTokenCache used by IConfidentialClientApplication. At this point, if you inspect ClaimsPrinciple.Current you will notice that the Identity is still unauthenticated and it has no claims, but MSALPerUserMemoryTokenCache needs the claims to work properly. Because of this sync problem, we are using the constructor that receives ClaimsPrincipal as argument and we are getting the claims from the object AuthorizationCodeReceivedNotification context. This object contains the property AuthenticationTicket.Identity, which is a ClaimsIdentity, created from the token received from Azure AD and has a full set of claims. / IConfidentialClientApplication confidentialClient = MsalAppBuilder.BuildConfidentialClientApplication(new ClaimsPrincipal(notification.AuthenticationTicket.Identity));

            // Upon successful sign in, get & cache a token using MSAL
            AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(Globals.Scopes, notification.Code).ExecuteAsync();

           // Azure 
            DisplayUserInfo(result);
        }
        catch (Exception ex)
        {
            throw new HttpResponseException(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.BadRequest,
                ReasonPhrase = $"Unable to get authorization code {ex.Message}."
            });
        }
    }

    private void DisplayUserInfo(AuthenticationResult authResult)
    {
        if (authResult != null)
        {
            var token = new JwtSecurityToken(jwtEncodedString: authResult.IdToken);
            // string expiry = token.Claims.First(c => c.Type == "expiry").Value;
            string userName = token.Claims.First(c => c.Type == "extension_userName").Value;
            string customRoles = token.Claims.First(c => c.Type == "extension_CustomRole").Value;

        }
    }

From sample code we know about we can access the id token. access token / claims. But that's not the issue.

We cant to call a end point using WepAPI for that we need to have access to the Access Token in the controller. Refer the following from sample code:-

public async Task Index() { try { // Retrieve the token with the specified scopes AuthenticationResult result = await AcquireTokenForScopes(new string[] { Globals.WriteTasksScope }); [This call is failing]

private async Task AcquireTokenForScopes(string[] scopes) { IConfidentialClientApplication cca = MsalAppBuilder.BuildConfidentialClientApplication(); var account = await cca.GetAccountAsync(ClaimsPrincipal.Current.GetB2CMsalAccountIdentifier()); return await cca.AcquireTokenSilent(scopes, account).ExecuteAsync(); } var account is coming as null.

My configuration of web api on Azure under appservice seems to be correct as I could access the end points by using postman (configured the postman and I can get access token)

Following is what you posted in previous post which is not clear to me at all

Next, look at the GetB2CMsalAccountIdentifier / GetAccountsCall. What account id does it compute. What is the value here? (this is the cache GET operation).

Note: I think I can alraedy spot a problem -

Could you please be little more explicit here?

spalmcc commented 1 year ago

@bgavrilMS

You had mentioned the following"-

Next, look at the GetB2CMsalAccountIdentifier / GetAccountsCall. What account id does it compute. What is the value here? (this is the cache GET operation).

And this is what "return $"{userObjectId}-{Globals.SignUpSignInPolicyId}.{tenantId}";" being returned.

Hope this would help.

spalmcc commented 1 year ago

@bgavrilMS any update would be appreciated. It's not to put any pressure jut wanted to know.

bgavrilMS commented 1 year ago

Hi @spalmcc - I have a PR out with a possible fix. Please try it out.

https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi/pull/150

MSALPerUserMemoryTokenCache is an old cache implementation that we forgot to fully remove. There is no need for a ClaimsPrincipal when you create the ConfidentialClientAPplication object. You only need ClaimsPrincipal to know what the account id is.

spalmcc commented 1 year ago

@bgavrilMS thanks alot. I will try and revert back.

spalmcc commented 1 year ago

@bgavrilMS

I tried and I am getting account as null in task controller.

private async Task<AuthenticationResult> AcquireTokenForScopes(string[] scopes)
        {
            IConfidentialClientApplication cca = MsalAppBuilder.BuildConfidentialClientApplication();
            var account = await cca.GetAccountAsync(ClaimsPrincipal.Current.GetB2CMsalAccountIdentifier()); // null
            return await cca.AcquireTokenSilent(scopes, account).ExecuteAsync();
        }
spalmcc commented 1 year ago

@bgavrilMS one more observation from Account controller, I tested the signout and I am getting account value as null , please check the comment in the following code. This might help you in some way.

public async Task SignOut()
        {
            // To sign out the user, you should issue an OpenIDConnect sign out request.
            if (Request.IsAuthenticated)
            {
                // await MsalAppBuilder.ClearUserTokenCache();
                // Remove all tokens from MSAL's cache
                var clientApp = MsalAppBuilder.BuildConfidentialClientApplication();
                string accountId = ClaimsPrincipal.Current.GetB2CMsalAccountIdentifier();
                IAccount account = await clientApp.GetAccountAsync(accountId); // account is null
                await clientApp.RemoveAsync(account);

                // Then sign-out from OWIN
                IEnumerable<AuthenticationDescription> authTypes = HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes();
                HttpContext.GetOwinContext().Authentication.SignOut(authTypes.Select(t => t.AuthenticationType).ToArray());
                Request.GetOwinContext().Authentication.GetAuthenticationTypes();
            }
        }
bgavrilMS commented 1 year ago

What is the value of ClaimsPrincipal.Current.GetB2CMsalAccountIdentifier() and what is the value of AuthenticationResult.Account.AccountId ? These 2 must match for the cache to be hit.

The accountID is made up of:

In many cases the tenant_id is not present in the ID token (i.e. ClaimsPrincipal) - claim tid. See the first known issue here which will tell you how to configure your app to add the claim.

spalmcc commented 1 year ago

@bgavrilMS

the format for ClaimsPrincipal.Current.GetB2CMsalAccountIdentifier() is as follows:- "userObjectId-SignUpSignInPolicyId.tenantId"

property "AuthenticationResult.Account.AccountId" is not there instead I found the "AuthenticationResult.Account.HomeAccountId" and the value is as follows:-

"{AccountId: <userObjectId>-<SignUpSignInPolicyId>.<tenant>}"

Also for your ease refer the following 3 screen shots:- image

image

image

Also I checked the ID Token and I do not see tenant id in that token. I will check how the same can be configured.

spalmcc commented 1 year ago

@bgavrilMS I am stuck with something at work so not able to further look into this. I will revert back in some time.

njannink commented 1 year ago

im seeing the same exception flying by using the example from Microsoft

vaibhavMethuku commented 1 year ago

I'm also facing the same issue. Do we have any solution for that.

bgavrilMS commented 1 year ago

Folks, we now have a sample up and running for ASP.NET + Microsoft.Identity.Web. This library provides higher level APIs and integration with both ASP.ENT and ASP.NET Core.

We don't have the B2C variant, but it should be fairly similar. Please see

https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect

dguerin commented 1 year ago

Having the same issue myself - code + deployment was working fine two months ago. I'm using a B2C standard sign in and sign up flow but the user is not authenticated after the sign up flow completes.

dguerin commented 1 year ago

Note to all - there seems to have been a bug for my issue that was resolved by bumping packages. I was using Blazor Server for my client UI, connecting to a .NET API. Here is what I've updated to:

    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.12" NoWarn="NU1605" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.12" NoWarn="NU1605" />