Open Closed

Identity server to AAD integration with seperate endpoint angular/core #109


User avatar
0
saintpoida created

Hi guys,

So I have got pretty much a base template running with a seperate Identity server endpoint with the angular/core project, not 100% of the advantage if its running from same db but i did it anyway to test it.

Now I am trying to add an external authentication to the identity server so the user can either create a local account or use their AAD account. I have managed to get the login with Active directory button to appear in the identity server endpoint and it looks like it works except when redirected back to my endpoint the user is actually not logged in. Instead it just shows the login screen again. The logs all say successful etc so im not sure if im missing something. I followed doc at https://docs.abp.io/en/abp/latest/How-To/Azure-Active-Directory-Authentication-MVC to get the majority of the settings in but its a little different cause im using the seperate endpoint.

So the following is what I have done

In ProjectNameIdentityServerModule i have

public override void PreConfigureServices(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); PreConfigure<AbpIdentityServerBuilderOptions(builder => { builder.UpdateJwtSecurityTokenHanderDefaultInboundClaimTypeMap = true; //no idea if this is needed but its closest thing i could find to the Jwt mapping instructions in the MVC link });

 PreConfigure<IIdentityServerBuilder>(builder => {
        builder.Services.AddAuthentication()
            .AddAzureAD(options => configuration.Bind("AzureAd", options));
        
        builder.Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options => {
            //this section is exactly as the mvc how to link is done so i have ommitted it
        });
    });
}

Then in that same project appSettings.json i have everything per the MVC also but two settings that may affect it i have included below "AzureAd": { "CallbackPath": "/signin-azuread-oidc", "Domain": "my AAD domain" //should this be empty? }

Since everything compiles and it launches the microsoft login as expected and then returns as expected i think all the above is correct configuration i will include the relevant lines from the log in a reply shortly.


19 Answer(s)
  • User Avatar
    0
    saintpoida created

    And the relevant log lines

    2020-04-17 11:50:03.521 +10:00 [INF] Executing ContentResult with HTTP Response ContentType of application/javascript 2020-04-17 11:50:03.523 +10:00 [INF] Executed action Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationScriptController.Get (Volo.Abp.AspNetCore.Mvc) in 453.2645ms 2020-04-17 11:50:03.524 +10:00 [INF] Executed endpoint 'Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationScriptController.Get (Volo.Abp.AspNetCore.Mvc)' 2020-04-17 11:50:03.524 +10:00 [INF] Request finished in 475.5367ms 200 application/javascript 2020-04-17 11:50:09.044 +10:00 [INF] Request starting HTTP/2.0 POST https://localhost:44315/Account/Login?handler=ExternalLogin application/x-www-form-urlencoded 225 2020-04-17 11:50:09.046 +10:00 [INF] CORS policy execution failed. 2020-04-17 11:50:09.046 +10:00 [INF] Request origin https://localhost:44315 does not have permission to access the resource. 2020-04-17 11:50:09.053 +10:00 [INF] No CORS policy found for the specified request. 2020-04-17 11:50:09.055 +10:00 [INF] Executing endpoint '/Account/Login' 2020-04-17 11:50:09.056 +10:00 [INF] Route matched with {page = "/Account/Login", area = "", action = "", controller = ""}. Executing page /Account/Login 2020-04-17 11:50:09.129 +10:00 [INF] Executing handler method Volo.Abp.Account.Public.Web.Pages.Account.LoginModel.OnPostExternalLogin - ModelState is "Invalid" 2020-04-17 11:50:09.136 +10:00 [INF] Executed handler method OnPostExternalLogin, returned result Microsoft.AspNetCore.Mvc.ChallengeResult. 2020-04-17 11:50:09.138 +10:00 [INF] Executing ChallengeResult with authentication schemes (["AzureAD"]). 2020-04-17 11:50:09.905 +10:00 [INF] AuthenticationScheme: AzureADOpenID was challenged. 2020-04-17 11:50:09.906 +10:00 [INF] Executed page /Account/Login in 849.9002ms 2020-04-17 11:50:09.906 +10:00 [INF] Executed endpoint '/Account/Login' 2020-04-17 11:50:10.018 +10:00 [DBG] Added 0 entity changes to the current audit log 2020-04-17 11:50:10.018 +10:00 [DBG] Added 0 entity changes to the current audit log 2020-04-17 11:50:10.021 +10:00 [INF] Request finished in 976.3547ms 302 2020-04-17 11:50:10.666 +10:00 [INF] Request starting HTTP/2.0 POST https://localhost:44315/signin-azuread-oidc application/x-www-form-urlencoded 2820 2020-04-17 11:50:10.667 +10:00 [INF] CORS policy execution successful. 2020-04-17 11:50:11.580 +10:00 [INF] AuthenticationScheme: Identity.External signed in. 2020-04-17 11:50:11.583 +10:00 [INF] Request finished in 916.6524ms 302 2020-04-17 11:50:11.608 +10:00 [INF] Request starting HTTP/2.0 GET https://localhost:44315/Account/Login?handler=ExternalLoginCallback
    2020-04-17 11:50:11.613 +10:00 [INF] Executing endpoint '/Account/Login' 2020-04-17 11:50:11.614 +10:00 [INF] Route matched with {page = "/Account/Login", area = "", action = "", controller = ""}. Executing page /Account/Login 2020-04-17 11:50:11.628 +10:00 [INF] Executing handler method Volo.Abp.Account.Public.Web.Pages.Account.LoginModel.OnGetExternalLoginCallbackAsync - ModelState is "Valid" 2020-04-17 11:50:11.632 +10:00 [WRN] External login info is not available 2020-04-17 11:50:11.636 +10:00 [INF] Executed handler method OnGetExternalLoginCallbackAsync, returned result Microsoft.AspNetCore.Mvc.RedirectToPageResult. 2020-04-17 11:50:11.637 +10:00 [INF] Executing RedirectToPageResult, redirecting to ./Login. 2020-04-17 11:50:11.638 +10:00 [INF] Executed page /Account/Login in 23.2993ms 2020-04-17 11:50:11.638 +10:00 [INF] Executed endpoint '/Account/Login' 2020-04-17 11:50:11.638 +10:00 [INF] Request finished in 30.0089ms 302 2020-04-17 11:50:11.646 +10:00 [INF] Request starting HTTP/2.0 GET https://localhost:44315/Account/Login
    2020-04-17 11:50:11.650 +10:00 [INF] Executing endpoint '/Account/Login' 2020-04-17 11:50:11.652 +10:00 [INF] Route matched with {page = "/Account/Login", area = "", action = "", controller = ""}. Executing page /Account/Login 2020-04-17 11:50:11.665 +10:00 [INF] Executing handler method Volo.Abp.Account.Public.Web.Pages.Account.LoginModel.OnGetAsync - ModelState is "Valid" 2020-04-17 11:50:11.668 +10:00 [INF] Executed handler method OnGetAsync, returned result . 2020-04-17 11:50:11.669 +10:00 [INF] Executing an implicit handler method - ModelState is "Valid" 2020-04-17 11:50:11.669 +10:00 [INF] Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult. 2020-04-17 11:50:11.751 +10:00 [DBG] Added bundle 'Lepton.Global' to the page in 2.60 ms. 2020-04-17 11:50:11.760 +10:00 [DBG] Added bundle 'Lepton.Global' to the page in 3.66 ms. 2020-04-17 11:50:11.761 +10:00 [INF] Executed page /Account/Login in 109.2622ms 2020-04-17 11:50:11.761 +10:00 [INF] Executed endpoint '/Account/Login'

  • User Avatar
    0
    saintpoida created

    Furthermore using the tip at the bottom of the MVC link I can confirm i am getting a full set of claims back from AAD.

    So im starting to think there is a step where a user is created on ABPs side that is missing for whatever reason or i have missed a call to some process to create the user cause there is nothing in any relevant tables related to the AAD user that successfully authenticated.

    Any thoughts?

    Thanks in advance

  • User Avatar
    0
    saintpoida created

    Ok after more moving bits and pieces around I have it working mostly

    So for the Identity Server only endpoint you need to put the following in PreConfigureServices

    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        var configuration = context.Services.GetConfiguration();
    
        PreConfigure<IIdentityServerBuilder>(builder =>
        {
            builder.Services.AddAuthentication()
                .AddAzureAD(options => configuration.Bind("AzureAd", options));
    
            builder.Services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
            {
                //this events is optional but can be useful for debugging the returned claims
                options.Events.OnTokenValidated = (async context =>
                {
                    var claims = context.Principal.Claims.ToList();
                    await Task.CompletedTask;
                });
                options.Authority = options.Authority + "/v2.0/";
                options.ClientId = configuration["AzureAd:ClientId"];
                options.CallbackPath = configuration["AzureAd:CallbackPath"];
                options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
                options.RequireHttpsMetadata = false;
                
                //you need client secret if your using a private app registration rather than public
                options.ClientSecret = configuration["AzureAd:ClientSecret"];
    
                options.TokenValidationParameters.ValidateIssuer = false;
                options.GetClaimsFromUserInfoEndpoint = true;
                options.SaveTokens = true;
                options.SignInScheme = IdentityConstants.ExternalScheme;
    
                options.Scope.Add("email");
            });
        });
    }
    

    Then in Configure services you need

    System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier);
    

    So mostly what is described in the MVC link but in different locations in the Identity Server only endpoint

    I also found some bugs in documentation at https://docs.abp.io/en/abp/latest/how-to/customize-signin-manager

    This line

    if (auth?.Principal == null || items == null || !items.ContainsKey("LoginProviderKey"))
    

    Should be

    if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey))
    

    Simlarly the line

    if (!items.ContainsKey("XsrfKey"))
    

    Should be

    if (!items.ContainsKey(XsrfKey))
    

    Note this bug only exists in the documentation, the source it is based on is correct.

    Finally my next related problem is that the Identity Server endpoint is now working however the front end for the primary app doesnt show the option to login with the external AAD?

    I have no idea how to make it appear as a button yet, I thought because everything was running through the Identity Server endpoint for Auth that it would be reflected, but it looks like its actually an Angular representation that looks identical. So is there a setting somewhere inside the angular project I can turn on so that it sees the same external auth methods of the identity server?

    Thanks

  • User Avatar
    0
    gterdem created
    Senior .NET Developer

    @saintpoida Hello,

    This line

    if (auth?.Principal == null || items == null || !items.ContainsKey("LoginProviderKey")) Should be

    if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey))

    It is not a bug; as it is stated in the docs

    ..we will be using a minorly modified version of the source code..

    Because those are private members as seen here: https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Identity/Core/src/SignInManager.cs#L22-L23

    Furhermore, the documentation is for MVC app, angular app will be different and 3rd party openid integration has not been implemented on angular yet.

  • User Avatar
    0
    saintpoida created

    @gterdem so that example is meant to fail then?

    It is using both a variable and a string representation, if what you said above is true then the following lines are incorrect in the same documentation

     var userId = items[XsrfKey] as string;
    

    and

    var provider = items[LoginProviderKey] as string;
    

    Is there someway i could proceed with the 3rd party integration into angular? Can we somehow mostly leverage the identity server endpoint from the angular app, by that i mean actually direct the not authenticated user to identity server endpoint instead of the angular login page, then back again when they log in?

  • User Avatar
    0
    gterdem created
    Senior .NET Developer

    @saintpoida thanks for point that out, we will fix the documentation.

    Currently, you can define a new client for your angular app in the identityserver with implicit flow and alter the authentication of angular app. This link may help: https://damienbod.com/2016/03/02/angular2-openid-connect-implicit-flow-with-identityserver4/

  • User Avatar
    0
    saintpoida created

    Hi @gterdem

    Ok so I have managed to mostly get what i want working, after much reading and playing around with different solutions i now have a different .net application, a blank angular project, abp io mvc and abp io angular all authenticating with 1 identity server. The angular edition is doing a full redirect to the identity server endpoint login page so that i can have external providers all work from one place.

    Thankfully the npm package you guys are using for oidc already works with the implicit and PKCE. However i have a few questions related to this

    To use PKCE with angular then in the identity server client setup i have to put RequirePKCE true and RequireClientSecret false, however i cant find anyway to do this through ABP Identity Server UI? I have done it direct in db but am wondering if there is some place in the UI i have missed that I could set these 2 options?

    Also exceptions thrown in the Identity Server endpoint just seem to show a blank page and I have to go to the logs.txt file of the Identity server to see the issue, things that i think should show a nice error to the user like their password being in the incorrect format? Is it supposed to do that or is that a bug?

    Sorry also forgot to add I want to replace the route /account/login in angular whats the best way to do this, I have done it with replaceable component however it still shows the page with tenant switch with my own content in the middle (where username password fields used to be)? I would prefer to replace the whole route without recompiling all the abp npm packages and source?

    The same goes for the AuthGuard, is there an easy/correct way i can inject another version so im not breaking the source code?

    Thanks again Pete

  • User Avatar
    0
    alper created
    Support Team Director

    hi @saintpoida,

    Can you show where you set RequirePKCE and RequireClientSecret values in the official Identity Server 4 Admin UI?

  • User Avatar
    0
    saintpoida created

    hi @alper

    No I didnt set them using the official admin UI, i did it direct in the database. I have not looked at the official UI only the ABP IO implementation?

    I will have a look at the official implementation and see if i can find them

  • User Avatar
    0
    saintpoida created

    hi @alper

    This documentation found at this link shows a screenshot with PKCE option, i will keep looking for RequireClientSecret

    https://www.identityserver.com/documentation/adminui/Clients/Editing_clients/#advanced-restrictions-configuration

    Im not sure how this helps unless ABP IO is built on top of that UI? Im happy to try and add the UI for it to ABP IO if it doesnt exist I am simply wondering if it is on a screen I have missed

  • User Avatar
    0
    saintpoida created

    Hi @alper

    After further investigation I can see the properties for both PKCE and RequireClientSecrets in the src code for the ABP identityserver domain object as well as the ETO object.

    https://github.com/abpframework/abp/tree/dev/modules/identityserver

    but i dont have the license to the UI modules to see if they are in there, maybe you can confirm? To reiterate I am simply wondering if the options exist and I cant find them or if they have not been added to the UI yet?

  • User Avatar
    0
    yekalkan created
    Support Team Fullstack Developer

    @saintpoida

    You are right. Those fields must be in UI and they will be there next release.

  • User Avatar
    0
    saintpoida created

    @yekalkan

    Ok cool!

    So now my last 2 questions really

    1. Exceptions thrown in the Identity Server endpoint (at least when its a separate endpoint, i have not tested a combined solution yet) seem to just be blank screens, im not sure if that is a bug or if its expected behaviour?
    2. Is there a way I can overwrite the account/login route in angular or inject a different authguard so that I can overwrite the path it goes to when someone not logged in? Right now I have managed to inject components to take place of the LoginComponent and TenantBoxComponent so that when they are redirect to the account/login page it just shows a please wait message but I am interested if there is a better way to do that? Or if I could inject a component to take over the Layout component of account/login page and make it even cleaner

    Thanks again

  • User Avatar
    0
    alper created
    Support Team Director

    hi @saintpoida,

    1- you can file an issue on https://github.com/abpframework/abp/issues with the repro steps. we'll check if it's a bug or not. 2- @mehmet can you answer for this ?

  • User Avatar
    0
    gterdem created
    Senior .NET Developer

    @saintpoida hello,

    Exceptions thrown in the Identity Server endpoint (at least when its a separate endpoint, i have not tested a combined solution yet) seem to just be blank screens, im not sure if that is a bug or if its expected behaviour?

    If you mean you can only check the error via logs but can't see a friendly error on UI about backend traffic, it is default behavior of identityserver logging. I am guessing it's high probably because of security concerns.

  • User Avatar
    0
    saintpoida created

    @gterdem hi

    What would be the best practise then to catch all the exceptions so I can wrap the relevant ones into User Friendly ones such as the password format when registering being incorrect. e.g if a user registers a new account with password of 'a' then thats invalid and they just get a blank screen on hitting register rather than a validation error.

    @alper thanks I will get a new solution and just confirm it is indeed reproducible and will log a bug if it is. I will also try a combined solution and see if its any different

  • User Avatar
    0
    Mehmet created

    Is there a way I can overwrite the account/login route in angular or inject a different authguard so that I can overwrite the path it goes to when someone not logged in? Right now I have managed to inject components to take place of the LoginComponent and TenantBoxComponent so that when they are redirect to the account/login page it just shows a please wait message but I am interested if there is a better way to do that? Or if I could inject a component to take over the Layout component of account/login page and make it even cleaner

    Hi @saintpoida

    Here is the Account module's components:

    | Component key | Description | |----------------------------------------------------|-------------------------| | Account.LoginComponent | Login page | | Account.RegisterComponent | Register page | | Account.ManageProfileComponent | Manage profile page | | Account.ForgotPasswordComponent | Forgot password page | | Account.ResetPasswordComponent | Reset password page | | Account.AccountComponent | The component that wraps register, login, forgot password, and reset password pages. Contains <router-outlet>. | | Account.ChangePasswordComponent | Change password form in manage profile page | | Account.PersonalSettingsComponent | Personal settings form in manage profile page | | Account.TenantBoxComponent | Tenant changing box in account component |

    Did you try to replace AccountComponent? I think you can do whatever you want by replacing this component.

    You can define your custom guard to AccountModule route definition in AppRoutingModule like below:

    {
    path: 'account',
    canActivate: [MyAuthGuard],
    loadChildren: () => import('@volo/abp.ng.account').then(m => m.AccountModule),
    },
    

    Or you can override the AuthGuard completely by adding a provide to AppModule like below:

    // app.module metadata
    providers: [{ provide: AuthGuard, useClass: MyAuthGuard }]
    
  • User Avatar
    0
    gterdem created
    Senior .NET Developer

    @saintpoida,

    What would be the best practise then to catch all the exceptions so I can wrap the relevant ones into User Friendly ones such as the password format when registering being incorrect. e.g if a user registers a new account with password of 'a' then thats invalid and they just get a blank screen on hitting register rather than a validation error.

    Notice that user registration validation is related with Identity management, not IdentityServer. Hence it is not related with IdentityServer logging or exception handling. If user is trying to register with an invalid character and getting a blank screen instead of a validation error; you are right. There should be a user friendly exception to notice the user about that. Would you mind to create an issue about the problem with detailed reproduce steps on github?

  • User Avatar
    0
    saintpoida created

    @Mehmet thanks did not realise i could replace whole account component, although i dont know if i still need forgot password and other pages other than login so i will try authguard injection first!

    @gterdem ok great i will try reproduce in new solution and then log an issue if thats the case

    Thanks again for all the help

Made with ❤️ on ABP v9.1.0-preview. Updated on November 11, 2024, 11:11