Merge pull request #282 from hchen2020/master

Improve UserIdentity implementation.
This commit is contained in:
Haiping 2024-01-31 13:59:03 -06:00 committed by GitHub
commit 44c29a2afe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 175 additions and 109 deletions

View file

@ -18,6 +18,7 @@ public interface IBotSharpRepository
#region User
User? GetUserByEmail(string email);
User? GetUserById(string id);
User? GetUserByUserName(string userName);
void CreateUser(User user);
#endregion

View file

@ -1,8 +1,10 @@
using BotSharp.Abstraction.Users.Models;
using System.Security.Claims;
namespace BotSharp.Abstraction.Users;
public interface IAuthenticationHook
{
Task<User> Authenticate(string id, string password);
void AddClaims(List<Claim> claims);
}

View file

@ -7,4 +7,5 @@ public interface IUserIdentity
string UserName { get; }
string FirstName { get; }
string LastName { get; }
string FullName { get; }
}

View file

@ -192,20 +192,17 @@ public class BotSharpDbContext : Database, IBotSharpRepository
#endregion
#region User
public User? GetUserByEmail(string email)
{
throw new NotImplementedException();
}
public User? GetUserByEmail(string email)
=> throw new NotImplementedException();
public User? GetUserById(string id)
{
throw new NotImplementedException();
}
public User? GetUserById(string id)
=> throw new NotImplementedException();
public void CreateUser(User user)
{
throw new NotImplementedException();
}
public User? GetUserByUserName(string userName)
=> throw new NotImplementedException();
public void CreateUser(User user)
=> throw new NotImplementedException();
#endregion

View file

@ -1,31 +1,35 @@
using BotSharp.Abstraction.Users.Models;
using System.IO;
namespace BotSharp.Core.Repository
namespace BotSharp.Core.Repository;
public partial class FileRepository
{
public partial class FileRepository
public User? GetUserByEmail(string email)
{
public User? GetUserByEmail(string email)
{
return Users.FirstOrDefault(x => x.Email == email);
}
return Users.FirstOrDefault(x => x.Email == email.ToLower());
}
public User? GetUserById(string id = null)
{
return Users.FirstOrDefault(x => x.ExternalId == id || x.Id == id);
}
public User? GetUserById(string id = null)
{
return Users.FirstOrDefault(x => x.Id == id || (x.ExternalId != null && x.ExternalId == id));
}
public void CreateUser(User user)
public User? GetUserByUserName(string userName = null)
{
return Users.FirstOrDefault(x => x.UserName == userName.ToLower());
}
public void CreateUser(User user)
{
var userId = Guid.NewGuid().ToString();
user.Id = userId;
var dir = Path.Combine(_dbSettings.FileRepository, "users", userId);
if (!Directory.Exists(dir))
{
var userId = Guid.NewGuid().ToString();
user.Id = userId;
var dir = Path.Combine(_dbSettings.FileRepository, "users", userId);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
var path = Path.Combine(dir, "user.json");
File.WriteAllText(path, JsonSerializer.Serialize(user, _options));
Directory.CreateDirectory(dir);
}
var path = Path.Combine(dir, "user.json");
File.WriteAllText(path, JsonSerializer.Serialize(user, _options));
}
}

View file

@ -4,11 +4,6 @@ using FunctionDef = BotSharp.Abstraction.Functions.Models.FunctionDef;
using BotSharp.Abstraction.Users.Models;
using BotSharp.Abstraction.Agents.Models;
using MongoDB.Driver;
using BotSharp.Abstraction.Routing.Models;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Repositories.Models;
using BotSharp.Abstraction.Routing.Settings;
using BotSharp.Abstraction.Evaluations.Settings;
using System.Text.Encodings.Web;
using BotSharp.Abstraction.Plugins.Models;
using BotSharp.Abstraction.Statistics.Settings;

View file

@ -28,4 +28,7 @@ public class UserIdentity : IUserIdentity
public string LastName
=> _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value!;
public string FullName
=> $"{FirstName} {LastName}".Trim();
}

View file

@ -12,34 +12,43 @@ public class UserService : IUserService
{
private readonly IServiceProvider _services;
private readonly IUserIdentity _user;
private readonly ILogger _logger;
public UserService(IServiceProvider services, IUserIdentity user)
public UserService(IServiceProvider services, IUserIdentity user, ILogger<UserService> logger)
{
_services = services;
_user = user;
_logger = logger;
}
public async Task<User> CreateUser(User user)
{
if (string.IsNullOrEmpty(user.UserName))
{
// generate unique name
var name = user.Email.Split("@").First() + "-" + Nanoid.Generate("0123456789botsharp", 6);
user.UserName = name;
}
else
{
user.UserName = user.UserName.ToLower();
}
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetUserByEmail(user.Email);
var record = db.GetUserByUserName(user.UserName);
if (record != null)
{
return record;
}
if (string.IsNullOrEmpty(user.Id))
{
user.Id = Guid.NewGuid().ToString();
}
record = user;
record.Email = user.Email.ToLower();
if (string.IsNullOrEmpty(user.UserName))
{
var name = record.Email.Split("@").First() + "-" + Nanoid.Generate("123456789botsharp", 6);
record.UserName = name;
}
else
{
record.UserName = user.UserName.ToLower();
}
record.Salt = Guid.NewGuid().ToString("N");
record.Password = Utilities.HashText(user.Password, record.Salt);
@ -53,18 +62,34 @@ public class UserService : IUserService
public async Task<Token> GetToken(string authorization)
{
var base64 = Encoding.UTF8.GetString(Convert.FromBase64String(authorization));
var (userEmail, password) = base64.SplitAsTuple(":");
var (id, password) = base64.SplitAsTuple(":");
var db = _services.GetRequiredService<IBotSharpRepository>();
var record = db.GetUserByEmail(userEmail);
var record = id.Contains("@") ? db.GetUserByEmail(id) : db.GetUserByUserName(id);
if (record == null)
{
record = db.GetUserByUserName(id);
}
if (record == null || record.Source != "internal")
{
// check 3rd party user
var validators = _services.GetServices<IAuthenticationHook>();
foreach (var validator in validators)
{
var user = await validator.Authenticate(userEmail, password);
if (user != null)
var user = await validator.Authenticate(id, password);
if (user == null)
{
continue;
}
if (string.IsNullOrEmpty(user.Source) || user.Source == "internal")
{
_logger.LogError($"Please set source name in the Authenticate hook.");
return null;
}
if (record == null)
{
// create a local user record
record = new User
@ -74,11 +99,12 @@ public class UserService : IUserService
FirstName = user.FirstName,
LastName = user.LastName,
Source = user.Source,
ExternalId = user.ExternalId
ExternalId = user.ExternalId,
Password = user.Password,
};
await CreateUser(record);
break;
}
break;
}
}
@ -105,26 +131,36 @@ public class UserService : IUserService
private string GenerateJwtToken(User user)
{
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.NameId, user.Id),
new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.GivenName, user.FirstName),
new Claim(JwtRegisteredClaimNames.FamilyName, user.LastName),
new Claim("source", user.Source),
new Claim("external_id", user.ExternalId),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var validators = _services.GetServices<IAuthenticationHook>();
foreach (var validator in validators)
{
validator.AddClaims(claims);
}
var config = _services.GetRequiredService<IConfiguration>();
var issuer = config["Jwt:Issuer"];
var audience = config["Jwt:Audience"];
var key = Encoding.ASCII.GetBytes(config["Jwt:Key"]);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtRegisteredClaimNames.NameId, user.Id),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.GivenName, user.FirstName),
new Claim(JwtRegisteredClaimNames.FamilyName, user.LastName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
}),
Expires = DateTime.UtcNow.AddMinutes(5),
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddHours(2),
Issuer = issuer,
Audience = audience,
SigningCredentials = new SigningCredentials
(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha512Signature)
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha512Signature)
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
@ -144,6 +180,16 @@ public class UserService : IUserService
{
var db = _services.GetRequiredService<IBotSharpRepository>();
var user = db.GetUserById(id);
if (user == null)
{
user = new User
{
Id = id,
FirstName = "Unknown",
LastName = "Anonymous",
Role = AgentRole.User
};
}
return user;
}
}

View file

@ -29,7 +29,7 @@ public class AgentController : ControllerBase
{
AgentIds = new List<string> { id }
});
return agents.Items.First();
return agents.Items.FirstOrDefault();
}
[HttpGet("/agents")]

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.Users.Models;
namespace BotSharp.OpenAPI.Controllers;
[Authorize]
@ -70,16 +72,29 @@ public class ConversationController : ControllerBase
var dialogs = new List<ChatResponseModel>();
foreach (var message in history)
{
var user = await userService.GetUser(message.SenderId);
dialogs.Add(new ChatResponseModel
if (message.Role == AgentRole.User)
{
ConversationId = conversationId,
MessageId = message.MessageId,
CreatedAt = message.CreatedAt,
Text = message.Content,
Sender = UserViewModel.FromUser(user)
});
var user = await userService.GetUser(message.SenderId);
dialogs.Add(new ChatResponseModel
{
ConversationId = conversationId,
MessageId = message.MessageId,
CreatedAt = message.CreatedAt,
Text = message.Content,
Sender = UserViewModel.FromUser(user)
});
}
else
{
dialogs.Add(new ChatResponseModel
{
ConversationId = conversationId,
MessageId = message.MessageId,
CreatedAt = message.CreatedAt,
Text = message.Content
});
}
}
return dialogs;

View file

@ -29,9 +29,9 @@ public class UserViewModel
{
return new UserViewModel
{
FirstName = "AI",
LastName = "Assistant",
Role = AgentRole.Assistant
FirstName = "Unknown",
LastName = "Anonymous",
Role = AgentRole.User
};
}

View file

@ -1,3 +1,5 @@
using BotSharp.Abstraction.Users.Models;
namespace BotSharp.Plugin.MongoStorage.Collections;
public class UserDocument : MongoBase
@ -14,4 +16,21 @@ public class UserDocument : MongoBase
public DateTime CreatedTime { get; set; }
public DateTime UpdatedTime { get; set; }
public User ToUser()
{
return new User
{
Id = Id,
UserName = UserName,
FirstName = FirstName,
LastName = LastName,
Email = Email,
Password = Password,
Salt = Salt,
Source = Source,
ExternalId = ExternalId,
Role = Role
};
}
}

View file

@ -7,38 +7,21 @@ public partial class MongoRepository
{
public User? GetUserByEmail(string email)
{
var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Email == email);
return user != null ? new User
{
Id = user.Id,
UserName = user.UserName,
FirstName = user.FirstName,
LastName = user.LastName,
Email = user.Email,
Password = user.Password,
Salt = user.Salt,
Source = user.Source,
ExternalId = user.ExternalId,
Role = user.Role
} : null;
var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Email == email.ToLower());
return user != null ? user.ToUser() : null;
}
public User? GetUserById(string id)
{
var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.Id == id || x.ExternalId == id);
return user != null ? new User
{
Id = user.Id,
UserName = user.UserName,
FirstName = user.FirstName,
LastName = user.LastName,
Email = user.Email,
Password = user.Password,
Salt = user.Salt,
Source = user.Source,
ExternalId = user.ExternalId,
Role = user.Role
} : null;
var user = _dc.Users.AsQueryable()
.FirstOrDefault(x => x.Id == id || (x.ExternalId != null && x.ExternalId == id));
return user != null ? user.ToUser() : null;
}
public User? GetUserByUserName(string userName)
{
var user = _dc.Users.AsQueryable().FirstOrDefault(x => x.UserName == userName.ToLower());
return user != null ? user.ToUser() : null;
}
public void CreateUser(User user)
@ -47,7 +30,7 @@ public partial class MongoRepository
var userCollection = new UserDocument
{
Id = Guid.NewGuid().ToString(),
Id = user.Id ?? Guid.NewGuid().ToString(),
UserName = user.UserName,
FirstName = user.FirstName,
LastName = user.LastName,