Open Closed

Update on External Identity Provider config for AAD B2C OpenID #6525


0
richardghubert created

Hi, could you provide me with an update to this:

https://forum.aspnetboilerplate.com/viewtopic.php?f=3&t=5140%20--%20https://stackoverflow.com/questions/48243612/asp-net-boilerplate-identityserver

I want to delegate the user sign-in flow to AAD B2C, i.e. to delegate identity management to Azure AD B2C by some AspNetZero-compatible means. Heres the appropriate tutorial from AAD B2C for this: https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-reference-oidc

We are using the newest AppNetZero ASP.NET-Core-MVC which is considerably different than the abp forum post above. In the doc, all I see is this:

https://docs.aspnetzero.com/documents/zero/latest/Development-Guide-Core#openid-connect-login

which I have done, as also described here:

https://tahirnaushad.com/2018/05/19/azure-ad-b2c-with-asp-net-core-2-0/

What is not yet clear to me:

  1. Do I have to add any redirect code myself to the AccountController.cs?
  2. After enabling OpenId in appsettings.json, what changes do I need to make to the IdentityServer config in that (or other) files.
  3. The Token Reply Url required in the Azure AAD B2C config should be what? I currently have https://localhost:62114/signin-oidc

Since I'm looking to delegate identity management to Azure AD B2C OpenId, the External Authetication Source described here (https://aspnetboilerplate.com/Pages/Documents/Zero/User-Management) does not appear to be the proper fit. I need to go via the OpenId-connect and, perhaps, in federation with the internal IdentityServer4?...

Thanks!


26 Answer(s)
  • 0
    ismcagdas created

    Hi @richardghubert

    Only thing you have to do is, filling the correct values for OpenId configuration in appsettings.json. We are using Microsoft's OpenIdConnect package and it handles return urls etc...

    ClientSecret parameter is not mandatory, you can leave it empty.

    "OpenId": {
      "IsEnabled": "false",
      "Authority": "",
      "ClientId": "",
      "ClientSecret": ""
    }
    
  • 0
    richardghubert created

    Thanks, ok. Will take a look again with only this single change and see why it wasn't working.

  • 0
    richardghubert created

    Getting back to this. The reply above and the default configuration in aspnetzero -- looks like -- is for Azure AD. I'm wanting to use Azure AD B2C which is somewhat different from AAD. Any tips/pointers appreciated.

    https://azure.microsoft.com/en-us/resources/samples/active-directory-b2c-dotnetcore-webapp/

    https://docs.microsoft.com/en-us/azure/active-directory-b2c/b2clogin

    I just want to use OpenIdConnect to Authenticate for starters.

  • 0
    ismcagdas created

    @richardghubert

    Have you tried setting authority to your Azure AD B2C url ? I haven't treid it with Azure AD B2C but according to this doc https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-reference-oidc they work similar to each other.

  • 0
    rbohac created

    @richardghubert or @ismcagdas

    Quick question on this, were you able to get it to work? I think I was able to get it to work (though having issues with api management... but that is a different topic).

    I also have one additional question around this, if I were to remove the internal login completely and just use the external login with b2c... I know this likely would need some changese especially with non-tenant users... Maybe a better question, do you know of any examples where someone has done that in abp and/or anz?

  • 0
    richardghubert created

    @rbohac Yes, it works via the OpenId-Connect approach, however, there is a caveat that I had to go to the .NET sources to figure out: the claims returned by AAD B2C do not have the name that is required by .NET-Core to complete the login. You must make the following change to your AuthConfigure.cs in order to map it to the actual claim required by .NET for the login (https://github.com/aspnet/Identity/blob/rel/2.0.0/src/Microsoft.AspNetCore.Identity/SignInManager.cs ). We use the AspNetZero users management after logging in since modifying that is not feasible.

    `if (bool.Parse(configuration["Authentication:OpenId:IsEnabled"])) { authenticationBuilder.AddOpenIdConnect(options => { options.ClientId = configuration["Authentication:OpenId:ClientId"]; options.Authority = configuration["Authentication:OpenId:Authority"]; options.SignedOutRedirectUri = configuration["App:WebSiteRootAddress"] + "Account/Logout"; options.ResponseType = OpenIdConnectResponseType.IdToken;

                    options.MetadataAddress =
                        "https://xxxxx.b2clogin.com/xxxxxTest.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_xxxxSignInPolicy";
    
                    options.GetClaimsFromUserInfoEndpoint = true;
                    options.ClaimActions.MapAll(); 
    
                    var clientSecret = configuration["Authentication:OpenId:ClientSecret"];
                    if (!clientSecret.IsNullOrEmpty())
                    {
                        options.ClientSecret = clientSecret;
                    }
    
                    options.Events = new OpenIdConnectEvents()
                    {
    
                        OnTokenValidated = (context) =>
                        {
    
                            var email = context.Principal.FindFirstValue("emails"); //initial test:emails => email first when multiple emails
                            ClaimsIdentity claimsId = context.Principal.Identity as ClaimsIdentity;
                            claimsId?.AddClaim(new Claim(ClaimTypes.NameIdentifier, $@"{email}"));
    
                            return Task.FromResult(0);
                        }
                    };
                });
            }`
    
  • 0
    rbohac created

    @richardghubert - Awesome, and thanks for the response. I got side tracked for another tasks, so it might be a few days before I have a chance tyo go back on this. Glad to hear you got it working and I also appreciate the info around how the claims issue, as I'm pretty sure that is the issue I'm having currently. Will let you know how it goes. One other question I do have though, are you first logging in through ANZ, and from there, choosing to go through the external open id connect? I was able to get that to work when using the ADD (issue i'm having was in b2c, but think the claim issue you specified will likely resolve that one).

    Anyways, thanks again for the info!

  • 0
    richardghubert created

    Yes, works like a charm with this adaptation to the .NET-core internals. You can just bypass (or re-implement) the ANZ login view to go directly to the OpenId-Connect where the user logs in directly to AAD B2C. The REST in the background happens then as always with OpenId-Connect.

  • 0
    rbohac created

    Finally got back to this... one quick quesiton though, were you able to get this to work if they start at the b2c end point and then redirecting it after logging in to the ANZ? Only ask this as I'm failing pretty hard on getting that part to work. That said, if I start at ANZ and then choose the external login and go to the b2c, then I can get it to work. I might be missing an obvious on this?

    ps: I apologize for having to ask this again. I spent a decent amount of time trying to get this to work... as I'd assume it shouldn't matter as it still ends up hitting the signin-oidc

    pss: I think you were able to get it to work from starting at the b2c point... but just want to make sure in case i'm wasting my time

  • 0
    konverto created

    Hello,

    I'm also trying to authenticate on aspnetzero with B2C. I've inserted the "if (bool.Parse(configuration["Authentication:OpenId:IsEnabled"]))" part in the AuthConfigurer, but it's not working. I still have some doubts about the correct configuration in appsettings.json. Could you give me some hints about it?

    I've tried different variants, but i just don't seem to get the right one..

    "OpenId": { "IsEnabled": "true", "ClientId": "azure-id-hash", "Authority": "http://mytenant.onmicrosoft.com/", "LoginUrl": "http://localhost:4200/B2C_1_login", "ValidateIssuer": "true" },

    What am I doing wrong? Am i missing something?
    Any help is appreciated, thank you very much in advance.

  • 0
    richardghubert created

    Hi, this in appsettings.json works (in conjunction with my post above):

    "OpenId": { "IsEnabled": "true", "Authority": "https://yourtenant.b2clogin.com/yourtenant.onmicrosoft.com/oauth2/v2.0/authorize", "ClientId": "xxxe511-f4b2-47sa-a3d3-79dedb4xxxx", "ValidateAudience": false }

  • 0
    konverto created

    thank you very much for your quick response!

    I've re-implemented the above code from scratch, but, like the first time i've tried to, i'm running into errors in importing classes.

    So i have a few questions:

    • did you replace the other code, leaving only the "if" statement you posted?
    • Could you please share your "using" statements?
    • which version are you using? (i'm running the asp.core & angular, v 6.9.1 with .NET core 2.2)

    thank you

  • 0
    richardghubert created

    Hi,

    1. the relevant things are above. No other caveats in this context
    2. ok, see below.
    3. 2019-02-04 ASP.NET CORE MVC & jQuery .NET Core 2.2 v6.5.0

    using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Abp.Extensions; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens;

    namespace ptw.de.AspNetZero.Web.Startup { public static class AuthConfigurer { public static void Configure(IServiceCollection services, IConfiguration configuration) { var authenticationBuilder = services.AddAuthentication();

            if (bool.Parse(configuration["Authentication:OpenId:IsEnabled"]))
            {
                authenticationBuilder.AddOpenIdConnect(options =>
                {
                    options.ClientId = configuration["Authentication:OpenId:ClientId"];
                    options.Authority = configuration["Authentication:OpenId:Authority"];
                    options.SignedOutRedirectUri = configuration["App:WebSiteRootAddress"] + "Account/Logout";
                    options.ResponseType = OpenIdConnectResponseType.IdToken;
    
                    //rht:
                    //options.RequireHttpsMetadata = false;
                    //rht:++ 
                    options.MetadataAddress =
                        "https://xxxxxx.b2clogin.com/xxxx.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_XXXSignInPolicy";
    
  • 0
    konverto created

    Hi Richard, thank you very much for your help!

  • 0
    konverto created

    Good morning,

    sorry I have to bother again, but I just don't seem to get this to work. I've tried several "project variants", and right now I'm trying with the same settings as @richardghubert (2019-02-04 ASP.NET CORE MVC & jQuery .NET Core 2.2 v6.5.0)

    When I run ANZ and click on the "open id connect" login button, it redirects me to the b2c and right back to the "http://localhost:62114/Account/Login?ReturnUrl=%2FApp", without even showing me the b2c login page.

    I followed the instructions provided and made the code changes in

    RvsMvcJquery65Demo\src\RvsMvcJquery65Demo.Web.Mvc\appsettings.json

     "OpenId": {
      "IsEnabled": "true",
      "Authority": "https://mytenant.b2clogin.com/mytenant.onmicrosoft.com/oauth2/v2.0/authorize",
      "ClientId": "myclientid",
      "ValidateAudience": false
    }
    

    and added the mentioned code to RvsMvcJquery65Demo\src\RvsMvcJquery65Demo.Web.Host\Startup\AuthConfigurer.cs

    if (bool.Parse(configuration["Authentication:OpenId:IsEnabled"]))
            {
                authenticationBuilder.AddOpenIdConnect(options =>
                {
                    options.ClientId = configuration["Authentication:OpenId:ClientId"];
                    options.Authority = configuration["Authentication:OpenId:Authority"];
                    options.SignedOutRedirectUri = configuration["App:WebSiteRootAddress"] + "Account/Logout";
                    options.ResponseType = OpenIdConnectResponseType.IdToken;
    
                    options.MetadataAddress = "https://mytenant.b2clogin.com/mytenant.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=b2c_1_login";
    
                    options.GetClaimsFromUserInfoEndpoint = true;
                    options.ClaimActions.MapAll();
    
                    var clientSecret = configuration["Authentication:OpenId:ClientSecret"];
                    if (!clientSecret.IsNullOrEmpty())
                    {
                        options.ClientSecret = clientSecret;
                    }
    
                    options.Events = new OpenIdConnectEvents()
                    {
    
                        OnTokenValidated = (context) =>
                        {
    
                            var email = context.Principal.FindFirstValue("emails"); //initial test:emails => email first when multiple emails
                            ClaimsIdentity claimsId = context.Principal.Identity as ClaimsIdentity;
                            claimsId?.AddClaim(new Claim(ClaimTypes.NameIdentifier, $@"{email}"));
    
                            return Task.FromResult(0);
                        }
                    };
                });
            }
    

    I just don't understand what I'm missing / doing wrong. How did you configure you Azure AD B2C "return url"? Any hints are appreciated, i know there MUST be a way to get this to work, since you guys already managed to.

    Thanks in advance

  • 0
    chris.tune created

    Hi All.

    I'm stuggling to get all the steps and need some guidance if anyone can help?

    For example: If you are using AzureAd or Azure B2C.

    I see that in the above example we are adding an additional claim / rewriting based on a claim from AzureAD/B2C.

    What are the claims that Zero looks for when resolving the user, for example if it's email there might actually be multiple idents with the same email, or the fact that it's not immutible?

    For Azure AD you might want to use oid to have the same id accross Apps or sub if you want different id's accross different apps.

    Do we have to use email? Can we override / configure zero to use a different claim for the id?

    Thoughts anyone?

    Chris

  • 0
    richardghubert created

    Hi Chris, I have the feeling that your issue now lies in the claims that you are defining on the AAD B2C side: in my examples above, it looks for specific claims, e.g. email, and if it doesn't find the things it needs, then you will be redirected back to try again. You most certaintly do not have to use email as your username claim. You can use any claim if you change the line in my code to pull that one out. Do not rely on the default claims expected by .NET (see above) since these are not present in the AAD B2C claim set. You'll need to work through this with AAD B2C portal ... that takes a little time at the start to figure out initially. Note that Azure AD and Azure AAD B2C work differently. E.g. B2C does not yet have the GraphAPI, but it B2C is low-cost, AAD is not...

    R

  • 0
    konverto created

    Hi Richard, do you maybe have any hints on the issue i posted above (yesterday)? any help is very appreciated!!

    Thanks in advance

  • 0
    richardghubert created

    @konverto The return to the Login page is just the default behavior if the login fails. As such, this could be due to any number or a combination of small things. To get to the precise location of the problem, you'll need to put a breakpoint in the places above (see my codes above) and check what is happeing. On the AAD B2C-side, you'll also want to check a few things. Note that you can test your flows directly in the AAD B2C portal. Here are my thoughts:

    1. In your code above: https://mytenant.b2clogin.com/mytenant.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=b2c_1_login"; Be sure to replace "mytenant" with YOUR b2c tenant name. Also be sure that you have the B2C flow "b2c_1_login" defined in the AAD B2C portal. You can test this there.

    2. be sure your browser cache and cookies storage are blanko-clean when testing. If not, you'll get all kinds of strange effects when debugging. Your case above could be due to an auto-login (SSO) due to a cookie in your browser that is then providing old state and results in a rejection.

    3. Be sure that your claim "email" is getting found in your code. Just put a breakpoint there and check the result...

    4. B2C Reply-URL example. All checkboxes are "yes" except for Native Client is "No", no optional required. it is optional. The URLs.

      1. https://localhost:44399/Home
      2. https://localhost:44399/signin-oidc (whereby this one is defaulted I believe, but I have it in there to be sure)

    Good luck!

  • 0
    chris.tune created

    OK cool that's awesome info so just confirming :

    ClaimTypes.NameIdentifier is the field that is used to lookup the login/user?

    I'm guessing this is the same for both Cookie and JWT?

    Chris

  • 0
    richardghubert created

    @chris.tune Yes, this is required by .NET-Core Identity. So if you don't want to modify the .NET sources, you had better use it :-) See above. However, it is not set by AAD B2C, that is the caveat I talk about above. In my code above, I define this claim and then set it to the user's email so that .NET does not reject the claim set. This will then be used as the AspNetZero username if I remember correctly, but that could be changed as well. The JWT(claimset) can be set in a cookie, you can check this in your browser's debugger, but cookie storage (the default I think) is only one option.

  • 0
    chris.tune created

    Thanks richardghubert!

  • 0
    chris.tune created

    OK Gents.

    Maybe Ive just screwed my code... as the userId is not resolving when I pass the bearer token to the api. It has the correct claim and I even added a jwt sub claim as well.

    I just want to double check. I'm wanting to use the bearer token using the JWT not a cookie as it's going to be called from external systems through the API.

    Does ABP only support openId as a cookie??

    Chris

  • 0
    richardghubert created

    @chris.tune Once you have a valid JWT, then you use it in your API calls to your server (Bearer). This is a big topic (lots of books and web sites doesn't make it simpler though), and is not ABP specific. In general your external (CORS) JS-client will need to use OpenID-Connect (via a JS-client-side-library or via another backend server) if it wants to send a valid, authentcated SSO JWT Bearer token to your server. Your server will then validate this token against the Identity Provider such as AAD B2C and cache it for a while.

    If your JS-client is not external, i.e. it is same-origin, then you have served the page from your server where the user logged in: this is our scenario above. In this case, you could use the cookie for storage, or the JWT token in the Bearer Header. Both of them will contain all the claims that you (normally via AAD B2C) put in that signed, valid token. This JTW token can be read without a signature (see jwt.ms for example) anywhere, by anybody. It just can't be modified.

    Hope that helps. More infos on stack exchange :-)

  • 0
    chris.tune created

    Sorry I didnt explain well.

    My bearer token validates OK, thats all good.

    I auth with AzureAd, great get the token back...tick I now want to use this in an api call using it as Bearer Authentication. I've done this many times before but not with ABP.

    We are in the same domain.. so CORS is OK.

    My challenge is that although im sending a azureAd OpenIdCOnnect bearer token to the api, the ABSession UserId is not getting set. Which I assumed would be determined from the claim nameidentifier.

    I'm was guessing in ABP somewhere it takes my name identifier and resolves this to the login then adds this to the ABPSession by adding it to the AbpClaimTypes.UserId claim... but maybe not?

    The outcome of this is that when I call an api controller the ABPSession.UserId it's null.

    I have set a http header for the Abp.TenantId in the api call just while I run locally and this is populated in the ABPSession.Tenant just fine.

    I'm just wondering if my code is wrong or if the ABP code which populates the UserId is based upon cookie auth middleware or something.

    I was just checking if pure Bearer Auth calls were compatible with OpenIdConnect tokens?

    I might need to create a middleware or even in the JWT events add the AbpClaimTypes.UserId claim which ABP seems to use.

    This is a lot more in depth than I thought it was going to be :D

    Chris

  • 0
    chris.tune created

    OK Guys for the time being I hacked around this.

    On each token validated event I lookup the ABP user with my oid and manually add the claim to the identity. This now comes through in each request in the API. I'll have to make this cached etc as it's pretty painful looking up a user each request :) I'll probably make this middlware at some point and will share the code.

    @richardghubert I want to say thanks for all your input and time. Appreciate your help.

    Chris