2019年7月23日 星期二

[ASP.NET Core] Identity Server 4 – LDAP authentication


 ASP.NET Core   Identity Server 4   OpenLDAP  


Introduction


After having the OpenLDAP container, we will use Identity Server 4 to build the Authentication Server which supports LDAP authentication.

The goals are
·   Create an Authentication Server with Identity Server 4
·   LDAP authentication with User name and password







Environment


docker-openldap 1.2.4 (OpenLDAP 2.4.47)
ASP.NET Core 2.2.203
IdentityServer4 2.4.0
Nordes/IdentityServer4.LdapExtension 2.1.8



Initialize Project


The source code is on my Github.


Create new dotnet project

$ dotnet new webapi --name AspNetCore.IdentityServer4.Auth
$ dotnet new sln --name AspNetCore.IdentityServer4
$ dotnet sln AspNetCore.IdentityServer4.sln add AspNetCore.IdentityServer4.Auth/AspNetCore.IdentityServer4.Auth.csproj


Install Nuget Packages




Or by dotnet CLI

$ cd AspNetCore.IdentityServer4.Auth
$ dotnet add package IdentityServer4 --version 2.4.0
$ dotnet add package IdentityServer.LdapExtension --version 2.1.8


Implement



Enable IdentityService4

Inject IdentityService4 service on Startup.

Startup.cs : ConfigureServices

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

   #region Identity Server

   var builder = services.AddIdentityServer(options =>
   {
       options.Events.RaiseErrorEvents = true;
       options.Events.RaiseInformationEvents = true;
       options.Events.RaiseFailureEvents = true;
       options.Events.RaiseSuccessEvents = true;
   });
   #endregion
}


And add IdentityServer4 to the pipeline.

Startup.cs : Configure

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
      app.UseIdentityServer();

      app.UseHttpsRedirection();
      app.UseMvc();
}


Define Resources and Clients

Definition
Description
Identity resources are data like user id/name, or email address of a user. An identity resource has a unique name, and we can assign arbitrary claim types to it.
To allow clients to request access tokens for APIs, you need to define API resources and register them as a scope.
Clients represent applications that can request tokens from your Identity Server.


There are two ways of defining IdentityServer4’s Resources and Clients:
1. Class file
2. JSON file (appsettings.json)

I suggest to use Class-file configuration since it’s strong typed, and we will use Class file in this example.


InMemoryInitConfig.cs                                                                              

First create the in-memory initialize configuration file,

$ cd AspNetCore.IdentityServer4.Auth
$ mkdir -p Utils/Config

Add the InMemoryInitConfig.cs as following,

public class InMemoryInitConfig
{
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Email(),
                new IdentityResources.Profile(),
                new IdentityResources.Phone(),
                new IdentityResources.Address()
            };
        }

        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new ApiResource[]
            {
                new ApiResource("MyBackendApi1", "My Backend API 1"),
                new ApiResource("MyBackendApi2", "My Backend API 2"),
            };
        }

        public static IEnumerable<Client> GetClients()
        {
            return new[]
            {
                // client credentials flow client
                new Client
                {
                    Enabled = true,
                    ClientId = "MyBackend",
                    ClientName = "MyBackend Client",
                    AllowedScopes = { "MyBackend1","MyBackend2" },
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    AccessTokenType = AccessTokenType.Jwt,
                    AlwaysSendClientClaims = true,
                    UpdateAccessTokenClaimsOnRefresh = true,
                    AlwaysIncludeUserClaimsInIdToken = true,
                    AllowAccessTokensViaBrowser = true,
                    IncludeJwtId = true,
                    ClientSecrets = { new Secret("secret".Sha256()) },

                    AllowOfflineAccess = true,
                    AccessTokenLifetime = 3600,               
               }
            };
        }
}

Here we define 2 API resources, "MyBackendApi1" and "MyBackend2" and put them to the scope of Client: "MyBackend", which uses Grant Types: ResourceOwnerPassword.

The Access token is JWT and will expires in 3600 seconds (1 hour).



Set LDAP connection configuration

appsettings.json

{
  "LdapServer": {
    "Url": "localhost",
    "Port": 389,
    "Ssl": false,
    "BindDn": "cn=admin,dc=example,dc=org",
    "BindCredentials": "admin",
    "SearchBase": "dc=example,dc=org",
    "searchFilter": "(&(objectClass=person)(uid={0}))"
  }
}


Add Signing credential

You can add the signing credential by AddSigningCredential method.

var builder = services.AddIdentityServer(options =>
            {
                // …
            });

builder.AddSigningCredential(…);


Here we will just use the temporary signing key as following,

Startup.cs : ConfigureServices

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

      #region Identity Server

      var builder = services.AddIdentityServer(options =>
            {
                // Skip …
            });

       // Signing credential
       builder.AddDeveloperSigningCredential();
       #endregion
}

Which will create a temporary key at run time.



Set in-memory code config

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

     #region Identity Server

     var builder = services.AddIdentityServer (options => {
                // Skip …     
     });

      // Signing credential
      builder.AddDeveloperSigningCredential ();

      // Set in-memory, code config
      builder.AddInMemoryIdentityResources (InMemoryInitConfig.GetIdentityResources ());
      builder.AddInMemoryApiResources (InMemoryInitConfig.GetApiResources ());
      builder.AddInMemoryClients (InMemoryInitConfig.GetClients ());
      builder.AddLdapUsers<OpenLdapAppUser> (this.Configuration.GetSection ("LdapServer"), UserStore.InMemory); // OpenLDAP

      #endregion
}


PS. If we use Active Directory instead of OpenLDAP, then we have to replace the last line of code to

builder.AddLdapUsers<ActiveDirectoryAppUser>(this.Configuration.GetSection("LdapServer"), UserStore.InMemory); // ActiveDirectory


Test IdentityServer4 by Discovery Endpoint

The Discovery Endpoint can be used to retrieve metadata about your Identity Server.
Now we can run the application and link to the following Discovery Endpoint url:

https://localhost:6001/.well-known/openid-configuration

You will see the below response if the connection is fine.



We can see that the supported scopes include “MyBackend1” and “MyBackend2” in below part of the JSON.




(Optional) Create a Sign-in API for LDAP authentication

Since we don’t have a Backend Server (Backend services) so far, we can at least create a LDAP authentication API to validate the user within the OpenLDAP.

First lets have a API model: LdapUser,

LdapUser.cs

public class LdapUser
    {
         /// <summary>
        /// User name
        /// </summary>
        public string Username { get; set; }

        /// <summary>
        /// User password
        /// </summary>
        public string Password { get; set; }
    }


Then create the API: SignIn, in a new ApiController, LdapController.cs


LdapController.cs (Source code)

using System.Threading.Tasks;
using AspNetCore.IdentityServer4.Auth.Models;
using IdentityServer.LdapExtension.UserModel;
using IdentityServer.LdapExtension.UserStore;
using IdentityServer4.Events;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

[Route("api/[controller]")]
[ApiController]
public class LdapController : ControllerBase
{
        private readonly ILdapUserStore userStore = null;

        public LdapController(
            ILdapUserStore userStore)
        {
            this.userStore = userStore;
        }

        [HttpPost("SignIn")]
        public async Task<IActionResult> SignIn([FromBody]LdapUser model)
        {
            // validate username/password against LDAP
            var user = this.userStore.ValidateCredentials(model.Username, model.Password);

            if (user != default(IAppUser))
            {
                // Response with authentication cookie
                await this.HttpContext.SignInAsync(user.SubjectId, user.Username);
                return this.Ok();
            }
            else
            {
                return this.Unauthorized();
            }
        }
}


Since I have a user, uid=jblin,dc=example,dc=org with password “123456” in the OpenLDAP container.




The sample Http request:

Title
Value
Http method
HttpPost
Http header
Content-Type: application/json
Http body
{
      "Username":"jblin",
      "Password":"123456"
}


Here is the successful-authorized response




Or the fail one





(Optional) Issue an JWT token

We can use IdentityServerTools class to create JWT tokens by IdentityServer token creation engine.
We will issue a JWT token to client in the previous Sign-in API.


LdapController.cs (Source code)

1. Inject the IdentityServerTools and use its method: IssueJwtAsync, to issue a JWT token.
2. Write the token to response.

 [Route("api/[controller]")]
 [ApiController]
 public class LdapController : ControllerBase
 {
        private readonly ILdapUserStore userStore = null;
        private readonly IdentityServerTools tools = null;

        public LdapController(
            ILdapUserStore userStore,
            IdentityServerTools tools)
        {
            this.userStore = userStore;
            this.tools = tools;
        }

        [HttpPost("SignIn")]
        public async Task<IActionResult> SignIn([FromBody]LdapUser model)
        {
            // validate username/password against Ldap
            var user = this.userStore.ValidateCredentials(model.Username, model.Password);

            if (user != default(IAppUser))
            {
                // Response with authentication cookie
                await this.HttpContext.SignInAsync(user.SubjectId, user.Username);

                // Get the Access token
                var accessToken = await this.tools.IssueJwtAsync(lifetime: 3600, claims: new Claim[] { new Claim(JwtClaimTypes.Audience, model.ApiResource) });

                // Write the Access token to response
                await this.HttpContext.Response.WriteAsync(accessToken);

                return this.Ok();
            }
            else
            {
                return this.Unauthorized();
            }
        }
 }


Notice that a specified name of API resource is a MUST parameter for issuing a JWT token.
So we add a new property "ApiResource" for API model, LdapUser.


Result:




Source code



Next step

We are going to create the Backend Server which will be secured by Identity Server 4.


(Appedix) NET Standard LDAP client library

The Nordes/IdentityServer4.LdapExtension has dependency on dsbenghe/Novell.Directory.Ldap.NETStandard, which is the LDAP client library works with any LDAP protocol compatible directory server (including Microsoft Active Directory).

Here is the sample code of an API which use dsbenghe/Novell.Directory.Ldap.NETStandard to authenticate user of OpenLDAP.

private async Task<bool> ExecLdapAuthAsync(string username, string password)
{
            var host = "localhost";
            var bindDN = "cn=admin,dc=example,dc=org";
            var bindPassword = "admin";
            var baseDC = "dc=example,dc=org";
            bool isAuthorized = false;

            try
            {
               isAuthorized = await Task.Run(() =>
               {
                   using (var connection = new Novell.Directory.Ldap.LdapConnection())
                   {
                       connection.Connect(host, Novell.Directory.Ldap.LdapConnection.DEFAULT_PORT);
                       connection.Bind(bindDN, bindPassword);

                       var searchFilter = $"(&(objectClass=person)(uid={username}))";
                       var entities = connection.Search(
                           baseDC,
                           LdapConnection.SCOPE_SUB,
                           searchFilter,
                           new string[] { "uid", "cn", "mail" },
                           false);

                       string userDn = null;

                       while (entities.hasMore())
                       {
                           var entity = entities.next();
                           var account = entity.getAttribute("uid");
                           if (account != null && account.StringValue == username)
                           {
                               userDn = entity.DN;
                               break;
                           }
                       }

                       if (string.IsNullOrWhiteSpace(userDn))
                       {
                           return false;
                       }

                       try
                       {
                           connection.Bind(userDn, password);
                           return connection.Bound;
                       }
                       catch (System.Exception)
                       {
                           return false;
                       }
                   }
               });

                return isAuthorized;
            }
            catch (Novell.Directory.Ldap.LdapException e)
            {
                throw e;
            }
}


Result:




Source Code



Reference








2 則留言: