ASP.NET
Core Identity Server 4 OpenLDAP
The goals are
· LDAP authentication with User name and password
▋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
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
▋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,
}
};
}
}
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
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
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
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.
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
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
網誌管理員已經移除這則留言。
回覆刪除網誌管理員已經移除這則留言。
回覆刪除Escape to a luxurious resort in jaipur, where royal elegance meets modern comfort. Enjoy world-class amenities, serene landscapes, and unforgettable experiences.
回覆刪除