Search code examples
c#asp.net-mvcidentityserver4aspnetboilerplate

How to map IdentityServer4 Identity to any WebApp (.Net MVC Boilerplate, .Net Core Boilerplate)


I'm creating an SSO server, to centralize all users in ActiveDirectory(AD) and manage them there instead of the database of each specific application.

To made this server I used IdentityServer4(Idsr4) with Ldap/AD Extension

I've setted the Idsr4 to use identity based on AD (this is "centralized identity"), and users now can login on Idsr4 with own AD login/ password

The question now is how to map the centralized identity to applications. I want to use same identity user in several applications.

I read through the documentation of IdentityServer4 but could not find anything related to a proposed structure.

Does anybody have a clear structure setup which could be used to understand the whole setup? (Separation like Asp.Net MVC Boilerplate, IdentityServer4, Protected Api.)

IdentityServer4 Config:

 public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        // configure identity server with in-memory stores, keys, clients and scopes
        services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            ////.AddSigningCredential(...) // Strongly recommended, if you want something more secure than developer signing (Read The Manual since it's highly recommended)
            .AddInMemoryIdentityResources(InMemoryInitConfig.GetIdentityResources())
            .AddInMemoryApiResources(InMemoryInitConfig.GetApiResources())
            .AddInMemoryClients(InMemoryInitConfig.GetClients())
            .AddLdapUsers<OpenLdapAppUser>(Configuration.GetSection("IdentityServerLdap"), UserStore.InMemory);
    }

IdentityServer4 InMemoryInitConfig:

namespace QuickstartIdentityServer{
public class InMemoryInitConfig
{
    // scopes define the resources in your system
    public static IEnumerable<IdentityResource> GetIdentityResources()
    {
        return new List<IdentityResource>
        {
            new IdentityResources.OpenId(),
            new IdentityResources.Profile(),
        };
    }

    public static IEnumerable<ApiResource> GetApiResources()
    {
        return new List<ApiResource>
        {
            new ApiResource("api1", "My API")
        };
    }

    // clients want to access resources (aka scopes)
    public static IEnumerable<Client> GetClients()
    {
        // client credentials client
        return new List<Client>
        {
            
            //DEMO HTTP CLIENT
            new Client
            {
                ClientId = "demo",
                ClientSecrets = new List<Secret> {new Secret("password".Sha256()) } ,
                ClientName = "demo",
                AllowedGrantTypes = {
                    GrantType.ClientCredentials, // Server to server
                    GrantType.ResourceOwnerPassword, // User to server
                    GrantType.Implicit
                },

                //GrantTypes.HybridAndClientCredentials,
                AllowAccessTokensViaBrowser = true,

                AllowOfflineAccess = true,
                AccessTokenLifetime = 90, // 1.5 minutes
                AbsoluteRefreshTokenLifetime = 0,
                RefreshTokenUsage = TokenUsage.OneTimeOnly,
                RefreshTokenExpiration = TokenExpiration.Sliding,
                UpdateAccessTokenClaimsOnRefresh = true,
                RequireConsent = false,

                RedirectUris = {
                    "http://localhost:6234/"
                },

                PostLogoutRedirectUris = { "http://localhost:6234" },
                AllowedCorsOrigins ={ "http://localhost:6234/" },

                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    "api1"
                },
                
            },

            

        };
    }
}

}

My client config:

public void Configuration(IAppBuilder app)

    {
        
        app.UseAbp();

        app.UseOAuthBearerAuthentication(AccountController.OAuthBearerOptions);

        // ABP
        //app.UseCookieAuthentication(new CookieAuthenticationOptions
        //{
        //    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        //    LoginPath = new PathString("/Account/Login"),
        //    // evaluate for Persistent cookies (IsPermanent == true). Defaults to 14 days when not set.
        //    //ExpireTimeSpan = new TimeSpan(int.Parse(ConfigurationManager.AppSettings["AuthSession.ExpireTimeInDays.WhenPersistent"] ?? "14"), 0, 0, 0),
        //    //SlidingExpiration = bool.Parse(ConfigurationManager.AppSettings["AuthSession.SlidingExpirationEnabled"] ?? bool.FalseString)
        //    ExpireTimeSpan = TimeSpan.FromHours(12),
        //    SlidingExpiration = true
        //});
        // END ABP

        /// IDENTITYSERVER
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = "Cookies"
        });

        app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
        {
            Authority = "http://localhost:5443", //ID Server
            ClientId = "demo",
            ClientSecret = "password",
            ResponseType = "id_token token",
            SignInAsAuthenticationType = "Cookies",
            RedirectUri = "http://localhost:6234/", //URL of website when cancel login on idsvr4
            PostLogoutRedirectUri = "http://localhost:6234", //URL Logout ??? << when this occor
            Scope = "openid",
            RequireHttpsMetadata = false,

            //AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,

        });
        /// END IDENTITYSERVER

        app.UseExternalSignInCookie("Cookies");
        
        app.MapSignalR();
    }

UPDATE

I was reading the documentation on OpenID Connect and saw that it is possible to create notifications for httpContext to take the user's claims in the Idsrv4 userinfo endpoint like this:

public void Configuration(IAppBuilder app)

    {
        
        app.UseAbp();

        // ABP
        //app.UseOAuthBearerAuthentication(AccountController.OAuthBearerOptions);

        //app.UseCookieAuthentication(new CookieAuthenticationOptions
        //{
        //    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        //    LoginPath = new PathString("/Account/Login"),
        //    // evaluate for Persistent cookies (IsPermanent == true). Defaults to 14 days when not set.
        //    //ExpireTimeSpan = new TimeSpan(int.Parse(ConfigurationManager.AppSettings["AuthSession.ExpireTimeInDays.WhenPersistent"] ?? "14"), 0, 0, 0),
        //    //SlidingExpiration = bool.Parse(ConfigurationManager.AppSettings["AuthSession.SlidingExpirationEnabled"] ?? bool.FalseString)
        //    ExpireTimeSpan = TimeSpan.FromHours(12),
        //    SlidingExpiration = true
        //});
        // END ABP

        /// IDENTITYSERVER
        AntiForgeryConfig.UniqueClaimTypeIdentifier = Thinktecture.IdentityModel.Client.JwtClaimTypes.Subject;
        JwtSecurityTokenHandler.DefaultInboundClaimFilter.Clear();

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = "Cookies"
        });

        // CONFIG OPENID
        var openIdConfig = new OpenIdConnectAuthenticationOptions
        {
            Authority = "http://localhost:5443", //ID Server
            ClientId = "demo",
            ClientSecret = "password",
            ResponseType = "id_token token",
            SignInAsAuthenticationType = "Cookies",
            RedirectUri = "http://localhost:6234/", //URL of website when cancel login on idsvr4
            PostLogoutRedirectUri = "http://localhost:6234", //URL Logout ??? << when this occor
            Scope = "openid profile api1",
            RequireHttpsMetadata = false,
            
            // get userinfo
            Notifications = new OpenIdConnectAuthenticationNotifications {
                SecurityTokenValidated = async n => {
                    var userInfoClient = new UserInfoClient(
                        new Uri(n.Options.Authority + "/connect/userinfo"),
                              n.ProtocolMessage.AccessToken);

                    var userInfo = await userInfoClient.GetAsync();
                    
                    // create new identity and set name and role claim type
                    var nid = new ClaimsIdentity(
                        n.AuthenticationTicket.Identity.AuthenticationType,
                        ClaimTypes.GivenName,
                        ClaimTypes.Role);

                    foreach (var x in userInfo.Claims) {
                        nid.AddClaim(new Claim(x.Item1, x.Item2));        
                    }

                    // keep the id_token for logout
                    nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));

                    // add access token for sample API
                    nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));

                    // keep track of access token expiration
                    nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));

                    // add some other app specific claim
                    //nid.AddClaim(new Claim("app_specific", "some data"));

                    n.AuthenticationTicket = new AuthenticationTicket(
                        nid,
                        n.AuthenticationTicket.Properties);

                    n.Request.Headers.SetValues("Authorization ", new string[] { "Bearer ", n.ProtocolMessage.AccessToken });

                }
            }

        };
        // END CONFIG OPENID

        app.UseOpenIdConnectAuthentication(openIdConfig);
        
        /// END IDENTITYSERVER

        app.UseExternalSignInCookie("Cookies");
        
        app.MapSignalR();
    }

UPDATE 2

Thank you @Khanh TO,

I did exactly what you recommended, I kept the database of each application

However to no longer manage the users by the application database, I hardcode a method that takes from the idsr4 userinfo endpoint

The information needed to create or update a user in the abpUsers table, then the application interprets the data and does the necessary actions

More specifically: In the redirect_uri I send to the AccountController of my client, there I have an ActionResult that does all this work calling the necessary methods to create/update an user on client userstable


Solution

  • I think the GrantType.ResourceOwnerPassword flow doens't support AD login and not support by the UseOpenIdConnectAuthentication neither , you can use Implicit or Hybrid flow.
    Once you authenticate the to your client mvc app, you can view any claims in HttpContext.User and find the correct claim value as user's identity (they are just claims , and no need to create a local account)