I refer to this post of mine which deals with the problem that I wanted to have the browser pop up a login form everytime a user logs on to the page. However, since my current problem is somehow different, I start this new post.
So I have this blazor server side app where I currently use Windows Authentication. For certain reasons, I have to replace WinAuth with form auth against our local active directory, to which I send user name and password and I get back the description of the user.
What I need to achieve is this:
- Send user/pass to the AD
- Read name, windows-login and group memberships from the result (I use group memberships in the app to provide some administration functionality, in case the user is member of a certain AD group)
- Tell the app that the user is logged in (= provided user/pass were valid).
My current code looks like that:
@layout LoginLayout
@page "/Login"
@using System.DirectoryServices
@using System.ComponentModel.DataAnnotations
@using System.Security.Claims
@using Microsoft.AspNetCore.Authentication.Cookies
@using Microsoft.AspNetCore.Http
@using Microsoft.AspNetCore.Identity
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject UserRecertificationContext userRecertContext
@inject NavigationManager navManager
@inject SignInManager<IdentityUser> SignInManager
@attribute [AllowAnonymous]
<div class="wrapper fadeInDown">
<div id="formContent">
<!-- Login Form -->
<EditForm Model="@userCredentials" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="row">
<div class="col-md-12">
<label>User Name :</label>
<input type="text" @bind-value="userCredentials.UserName" id="login" class="fadeIn second" placeholder="login" />
<ValidationMessage For="@(()=> userCredentials.UserName)" />
</div>
<div class="col-md-12">
<label>Password</label>
<input type="password" @bind-value="userCredentials.Password" id="password" class="fadeIn third" placeholder="password" />
<ValidationMessage For="@(()=> userCredentials.Password)" />
</div>
<input type="submit" class="fadeIn fourth" value="Log In">
</div>
</EditForm>
</div>
</div>
@if (showAuthenticationError)
{
<div class="alert alert-danger" role="alert">
<p>@authenticationErrorText</p>
</div>
}
@code {
private bool showAuthenticationError { get; set; } = false;
private string authenticationErrorText = "";
private AuthenticationUserModel userCredentials { get; set; } = new AuthenticationUserModel();
private async void HandleValidSubmit()
{
DirectoryEntry entry = new DirectoryEntry();
entry = new DirectoryEntry("LDAP://mydomain.local");
entry.Username = userCredentials.UserName;
entry.Password = userCredentials.Password;
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(SAMAccountName=" + userCredentials.UserName + ")";
SearchResult result = search.FindOne();
if (result == null)
{
//return false;
}
else
{
var claims = new List<Claim>();
string role = "";
ResultPropertyCollection fields = result.Properties;
claims.Add(new Claim(ClaimTypes.Name, result.GetDirectoryEntry().Name));
claims.Add(new Claim(ClaimTypes.WindowsAccountName,
result.GetDirectoryEntry().Username));
}
}
}
So far, so good. I managed to get the user from the AD when I enter user/pass and click the log in button. I can get the Name and login name from the result. And I'm sure that by iterating through the result, I can get the group memberships. What I'm struggling is to tell the app that this user can be "logged in".
Reading tutorials the entire day, I'm completely lost at the moment. In other tutorials, they used the SignInManager, like that:
await SignInManager.SignInWithClaimsAsync(user, isPersistent: false, claims);
NavigationManager.NavigateTo("/");
But I could not get that running b/c the corresponding part in startup.cs is missing but I can't get it right.
EDIT: Ok, i've found this tutorial which I tried to follow step by step. I'm now at the point where I have
a LoginController:
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class LoginController : ControllerBase
{
private IConfiguration _config;
public LoginController(IConfiguration config)
{
_config = config;
}
[AllowAnonymous]
[HttpGet]
public IActionResult Login(string userId, string pass)
{
AdUserModel login = new AdUserModel();
login.UserName = userId;
login.Password = pass;
IActionResult response = Unauthorized();
var user = AuthenticateUser(login);
if (user != null)
{
var tokenString = GenerateJSONWebToken(user);
response = Ok(new { token = tokenString });
}
return response;
}
private AdUserModel AuthenticateUser(AdUserModel login)
{
//var user = new AdUserModel();
DirectoryEntry entry = new DirectoryEntry();
entry = new DirectoryEntry("LDAP://mydomain.local");
entry.Username = login.UserName;
entry.Password = login.Password;
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(SAMAccountName=" + entry.Username + ")";
SearchResult result = search.FindOne();
if (result != null)
{
login.UserName = result.GetDirectoryEntry().Name.Split("CN=")[1];
login.UserLogin = result.GetDirectoryEntry().Username;
}
return login;
}
private string GenerateJSONWebToken(AdUserModel userInfo)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userInfo.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
//new Claim(ClaimTypes.Role, userInfo.Role)
};
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Issuer"],
claims,
expires: DateTime.Now.AddMinutes(10),
signingCredentials: credentials);
var encodetoken = new JwtSecurityTokenHandler().WriteToken(token);
return encodetoken;
}
[Authorize(Roles="Admin")]
[HttpPost]
public IActionResult Post([FromBody] string value)
{
try
{
return Ok(value);
}
catch (Exception)
{
return NotFound();
}
}
}
And I have a ServiceComponent, which, to be honest I do not know what it does:
public interface IServiceComponent
{
IRestResponse ResponseJson(string url, object requestBOdy, Dictionary<string, string> requestHeader, List<Parameter> requestParameter, Method method);
IRestResponse ResponseJsonAuth(string url, object requestBOdy, Dictionary<string, string> requestHeader, List<Parameter> requestParameter, Method method);
}
public class ServiceComponent : IServiceComponent
{
public IRestResponse ResponseJson(string url, object requestBody, Dictionary<string, string> requestHeader, List<Parameter> requestParameter, Method method)
{
var client = new RestClient(url);
var request = new RestRequest(method)
{
RequestFormat = DataFormat.Json,
JsonSerializer = new CamelCaseSerializer()
};
if (requestHeader != null)
{
foreach (var item in requestHeader)
{
request.AddHeader(item.Key, item.Value);
}
}
if (requestParameter != null)
{
foreach (var item in requestParameter)
{
request.AddParameter(item);
}
}
if (requestBody != null)
{
request.AddJsonBody(requestBody);
}
IRestResponse response = client.Execute(request);
return response;
}
public IRestResponse ResponseJsonAuth(string url, object requestBody, Dictionary<string, string> requestHeader, List<Parameter> requestParameter, Method method)
{
var client = new RestClient(url);
var request = new RestRequest(method)
{
RequestFormat = DataFormat.Json,
JsonSerializer = new CamelCaseSerializer()
};
if (requestHeader != null)
{
foreach (var item in requestHeader)
{
request.AddHeader(item.Key, item.Value);
}
}
if (requestBody != null)
{
request.AddJsonBody(requestBody);
}
IRestResponse response = client.Execute(request);
return response;
}
public class CamelCaseSerializer : ISerializer
{
public string ContentType { get; set; }
public CamelCaseSerializer()
{
ContentType = "appliocation/json";
}
public string Serialize(object obj)
{
var camelCaseSetting = new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
string json = JsonConvert.SerializeObject(obj, camelCaseSetting);
return json;
}
}
}
And I have the login component where I enter the credentials and trigger the Login controller:
@layout LoginLayout
@page "/Login"
@using System.DirectoryServices
@using Newtonsoft.Json
@using Services
@using System.ComponentModel.DataAnnotations
@using System.Security.Claims
@using Microsoft.AspNetCore.Authentication.Cookies
@using Microsoft.AspNetCore.Http
@using Microsoft.AspNetCore.Identity
@using System.Security.Principal
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject UserRecertificationContext userRecertContext
@inject NavigationManager navManager
@attribute [AllowAnonymous]
@inject IJSRuntime _JsRuntime
@inject ServiceComponent Service
<div class="wrapper fadeInDown">
<div id="formContent">
<!-- Icon -->
<!-- Login Form -->
<EditForm Model="@userCredentials" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="row">
<div class="col-md-12">
<label>User Name :</label>
<input type="text" @bind-value="userCredentials.UserName" id="login" class="fadeIn second" placeholder="login" />
<ValidationMessage For="@(()=> userCredentials.UserName)" />
</div>
<div class="col-md-12">
<label>Password</label>
<input type="password" @bind-value="userCredentials.Password" id="password" class="fadeIn third" placeholder="password" />
<ValidationMessage For="@(()=> userCredentials.Password)" />
</div>
<input type="submit" class="fadeIn fourth" value="Log In">
</div>
</EditForm>
</div>
</div>
@if (showAuthenticationError)
{
<div class="alert alert-danger" role="alert">
<p>@authenticationErrorText</p>
</div>
}
@code {
private bool showAuthenticationError { get; set; } = false;
private string authenticationErrorText = "";
private AuthenticationUserModel userCredentials { get; set; } = new AuthenticationUserModel();
string Message;
[Parameter]
public string Bearer { get; set; }
private async void HandleValidSubmit()
{
DirectoryEntry entry = new DirectoryEntry();
entry = new DirectoryEntry("LDAP://mydomain.local");
entry.Username = userCredentials.UserName;
entry.Password = userCredentials.Password;
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(SAMAccountName=" + userCredentials.UserName + ")";
SearchResult result = search.FindOne();
if (result == null)
{
//return false;
}
else
{
var user = new IdentityUser
{
UserName = result.GetDirectoryEntry().Name
};
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, result.GetDirectoryEntry().Name));
claims.Add(new Claim(ClaimTypes.WindowsAccountName, result.GetDirectoryEntry().Username));
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
CreateToken();
PostMethod();
}
}
public void CreateToken()
{
Message = "";
Bearer = "";
var response = Service.ResponseJson("https://localhost:44335/api/login?userId="+userCredentials.UserName + "&pass="+userCredentials.Password, null, null, null, RestSharp.Method.GET);
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
var json = JsonConvert.DeserializeObject<Dictionary<string, string>>(response.Content)["token"].ToString();
Bearer = json;
Message = "Create Token Success: " + response.StatusCode.ToString();
}
else
{
Message = "Create token Error: " + response.StatusCode.ToString();
}
}
public void PostMethod()
{
Message = "";
Dictionary<string, string> header = new Dictionary<string, string>();
header.Add("Authorization", "Bearer" + Bearer);
var value = Guid.NewGuid().ToString();
var response = Service.ResponseJsonAuth("https://localhost:44335/api/login", value, null, null, RestSharp.Method.GET);
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
Message = "Post Success:" + response.StatusCode.ToString();
}
else
{
Message = "Post Error:" + response.StatusCode.ToString();
}
}
public class AuthenticationUserModel
{
[Required(ErrorMessage = "Username is required.")]
public string UserName { get; set; }
[Required(ErrorMessage = "Password is required.")]
public string Password { get; set; }
}
}
The login method of the controller generates me a token, which will be sent to the server.
But when the ResponseJsonAuth method is executed, the resonse object has StatusCode 0 and from there I don't have a clue what to do next. I navigate to other pages but can't confirm whether this is working as expected or not... It is boggling my mind that something that should be an absolute basic feature like authentication must be so damn complex that it literally takes days to even have a small clue about what is going on here.

