420.7 Keycloak and Abp MVC or Abp Angular - chempkovsky/CS82ANGULAR GitHub Wiki

Notes

  • KeyCloak can be used as authentication and authorisation server
    • Once an application is registered with KeyCloak, you can define one or more roles for that application using the KeyCloak console.
    • Once you have created a user in KeyCloak, you can assign one or more registered application roles to that user.
    • After login into MVC app you can get all the roles assigned to the current user. KeyCloak sends the roles only for the current app. (In case you registered more than one apps and assigned the roles of other apps to the current user).
    • you don't have to rely on scopes
    • In short, KeyCloak made with love.
  • KeyCloak can be used as authentication only server
    • In this case, KeyCloak will be used to authenticate the user and Microsoft Identity middleware to authorize the user.
  • In this article we show how to configure Abp MVC (Abp Angular) app to use KeyCloak as authentication only server

Keycloak realm

Keycloak Mvc01 app

Keycloak Mvc02 app

Keycloak testuser

hosts file

  • We do not use hosts-file as yti was described in the article
    • applicationUrl will be untouched. Too many changes in Abp-app code will be required, since OpenIdDict server will need to be reconfigured.
    • instead we will use localhost:port as is

Create AbpMvc01 app

  • start cmd and run the command:
abp new rupbes.AbpMvc01 --template app --ui-framework mvc --theme leptonx-lite  --create-solution-folder --no-tests
  • open AbpMvc01WebModule.cs-file of the rupbes.AbpMvc01.Web-project and modify private void ConfigureAuthentication(ServiceConfigurationContext context) as follows:
Click to show the code
    private void ConfigureAuthentication(ServiceConfigurationContext context)
    {
        context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
        context.Services.Configure<AbpClaimsPrincipalFactoryOptions>(options =>
        {
            options.IsDynamicClaimsEnabled = true;
        });
        context.Services.AddAuthentication()
        .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "SSO with Keycloak", o =>
        {
            o.SignInScheme = IdentityConstants.ExternalScheme;
            o.SignOutScheme = IdentityConstants.ApplicationScheme;


            o.ClientId = "Mvc01";
            o.ClientSecret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; // copy Secret from keycloak console
            o.Authority = "https://kc.rupbes.by:8445/realms/rupbes.test";

            // default value == "/signin-oidc"
            // Keycloak  "Valid redirect URIs"-param must be set to "/signin-oidc"
            o.CallbackPath = new PathString("/signin-oidc");


            // Requests received on this path will cause the handler to invoke SignOut using the SignOutScheme.
            //o.RemoteSignOutPath = new PathString("/Home/DoLogout");              // default value


            // default value == "/signout-callback-oidc"
            // Keycloak  "Valid post logout redirect URIs"-param must be set to "/signout-callback-oidc"
            //            o.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
            /// <summary>
            /// The uri where the user agent will be redirected to after application is signed out from the identity provider.
            /// The redirect will happen after the SignedOutCallbackPath is invoked.
            /// </summary>
            /// <remarks>This URI can be out of the application's domain. By default it points to the root.</remarks>
            //             o.SignedOutRedirectUri = new PathString("/Home/DoCleanUpCookie");                       // default value


            o.ResponseType = OpenIdConnectResponseType.Code;
            o.UsePkce = true;

            // o.Scope.Add("user.read");

            // o.MapInboundClaims = false;
            o.Scope.Clear();
            o.Scope.Add("openid");
            o.Scope.Add("profile");
            o.Scope.Add("email");
            o.Scope.Add("phone");
            //  o.Scope.Add("offline_access");

            o.SaveTokens = true;
        });

    }
  • open the launchSettings.json file and copy the value of applicationUrl without the last backslash. In our case, we have "applicationUrl": "https://localhost:44366/", so we copy https://localhost:44366.

  • in the keycloak console insert copied value

    • Access settings
      • Root URL: https://localhost:44366
      • Home URL: https://localhost:44366
  • Start rupbes.AbpMvc01.Web-app and clock login.

    • On the login page click SSO with Keycloak-button and login into Keycloak as testuser
      • You will be back on Login page of the rupbes.AbpMvc01.Web-app
        • click register
  • logout from the app

    • try to login with SSO with Keycloak-button click
      • second time it will not ask for password.
  • to remind the user about complete logout we have to override Abp logout page

  • for rupbes.Abp2Keycloak.Web project create a folder Page/Account

  • In the Page/Account - folder create CustomLogoutModelModel-class (not a Razor page !!!) with a code as shown bellow

Click to show the code
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Threading.Tasks;
using Volo.Abp.Account.Settings;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.Settings;

namespace rupbes.Abp2Keycloak.Web.Pages.Account
{
    [Volo.Abp.DependencyInjection.Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(LogoutModel))]
    public class CustomLogoutModelModel : LogoutModel
    {
        public override async Task<IActionResult> OnGetAsync()
        {
            Claim? clm = this.User?.FindFirst("http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod");

            await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
            {
                Identity = IdentitySecurityLogIdentityConsts.Identity,
                Action = IdentitySecurityLogActionConsts.Logout
            });

            await SignInManager.SignOutAsync();

            if ("OpenIdConnect".Equals(clm?.Value, StringComparison.OrdinalIgnoreCase))
            {
                return new RedirectResult("https://kc.rupbes.by:8445/realms/rupbes.test/account");
            }

            if (ReturnUrl != null)
            {
                return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash);
            }

            if (await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin))
            {
                return RedirectToPage("/Account/Login");
            }

            return RedirectToPage("/");
        }
    }
}
  • after logout it will show Keycloak account page

Create AbpAng01 app

  • start cmd and run the command:
abp new rupbes.AbpAng01 --template app --ui-framework angular --theme leptonx-lite  --create-solution-folder --no-tests
  • open AbpAng01HttpApiHostModule.cs-file of the rupbes.AbpAng01.HttpApi.Host-project and modify private void ConfigureAuthentication(ServiceConfigurationContext context) as follows:
Click to show the code
    private void ConfigureAuthentication(ServiceConfigurationContext context)
    {
        context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
        context.Services.Configure<AbpClaimsPrincipalFactoryOptions>(options =>
        {
            options.IsDynamicClaimsEnabled = true;
        });
        context.Services.AddAuthentication()
                .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, "SSO with Keycloak", o =>
                {
                    o.SignInScheme = IdentityConstants.ExternalScheme;
                    o.SignOutScheme = IdentityConstants.ApplicationScheme;


                    o.ClientId = "Mvc02";
                    o.ClientSecret = "YYYYYYYYYYYYYYYYYYYYYYYYYYYYY"; // copy Secret from keycloak console
                    o.Authority = "https://kc.rupbes.by:8445/realms/rupbes.test";

                    // default value == "/signin-oidc"
                    // Keycloak  "Valid redirect URIs"-param must be set to "/signin-oidc"
                    o.CallbackPath = new PathString("/signin-oidc");


                    // Requests received on this path will cause the handler to invoke SignOut using the SignOutScheme.
                    //o.RemoteSignOutPath = new PathString("/Home/DoLogout");              // default value


                    // default value == "/signout-callback-oidc"
                    // Keycloak  "Valid post logout redirect URIs"-param must be set to "/signout-callback-oidc"
                    //            o.SignedOutCallbackPath = new PathString("/signout-callback-oidc");
                    /// <summary>
                    /// The uri where the user agent will be redirected to after application is signed out from the identity provider.
                    /// The redirect will happen after the SignedOutCallbackPath is invoked.
                    /// </summary>
                    /// <remarks>This URI can be out of the application's domain. By default it points to the root.</remarks>
                    //             o.SignedOutRedirectUri = new PathString("/Home/DoCleanUpCookie");                       // default value


                    o.ResponseType = OpenIdConnectResponseType.Code;
                    o.UsePkce = true;

                    // o.Scope.Add("user.read");

                    // o.MapInboundClaims = false;
                    o.Scope.Clear();
                    o.Scope.Add("openid");
                    o.Scope.Add("profile");
                    o.Scope.Add("email");
                    o.Scope.Add("phone");
                    //  o.Scope.Add("offline_access");

                    o.SaveTokens = true;
                });
    }
  • open the launchSettings.json file and copy the value of applicationUrl without the last backslash. In our case, we have "applicationUrl": "https://localhost:44374", so we copy https://localhost:44374.

  • in the keycloak console insert copied value for MVC02 app

    • Access settings
      • Root URL: https://localhost:44374
      • Home URL: https://localhost:44374
    • Note: Angular app has a Home URL=http://localhost:4200, but we set https://localhost:44374 (!!!) and Client authentication=ON (!!!)
  • Start rupbes.AbpAng01.HttpApi.Host-app, start angular app and clock login.

  • On the login page click SSO with Keycloak-button and login into Keycloak as testuser

    • You will be back on Login page of the rupbes.AbpMvc01.Web-app
      • click register
    • logout from the app
    • try to login with SSO with Keycloak-button click
      • second time it will not ask for password.
  • to remind the user about complete logout we have to override Abp logout page

  • for rupbes.Abp2Keycloak.Web project create a folder Page/Account

  • In the Page/Account - folder create CustomLogoutModelModel-class (not a Razor page !!!) with a code as shown bellow

Click to show the code
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Threading.Tasks;
using Volo.Abp.Account.Settings;
using Volo.Abp.Account.Web.Pages.Account;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.Settings;

namespace rupbes.AbpAng01.Pages.Account
{
    [Volo.Abp.DependencyInjection.Dependency(ReplaceServices = true)]
    [ExposeServices(typeof(LogoutModel))]
    public class CustomLogoutModelModel : LogoutModel
    {
        public override async Task<IActionResult> OnGetAsync()
        {
            Claim? clm = this.User?.FindFirst("http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod");

            await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
            {
                Identity = IdentitySecurityLogIdentityConsts.Identity,
                Action = IdentitySecurityLogActionConsts.Logout
            });

            await SignInManager.SignOutAsync();

            if ("OpenIdConnect".Equals(clm?.Value, StringComparison.OrdinalIgnoreCase))
            {
                return new RedirectResult("https://kc.rupbes.by:8445/realms/rupbes.test/account");
            }

            if (ReturnUrl != null)
            {
                return await RedirectSafelyAsync(ReturnUrl, ReturnUrlHash);
            }

            if (await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin))
            {
                return RedirectToPage("/Account/Login");
            }

            return RedirectToPage("/");
        }
    }
}
  • after logout it will show Keycloak account page
⚠️ **GitHub.com Fallback** ⚠️