diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs index b9b8b932..8e5852cc 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserIdentity.cs @@ -14,17 +14,27 @@ public class UserIdentity : IUserIdentity } - public string Id + public string Id => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier)?.Value!; public string UserName => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Name)?.Value!; - public string Email + public string Email => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Email)?.Value!; - public string FirstName - => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.GivenName)?.Value!; + public string FirstName + { + get + { + var givenName = _claims?.FirstOrDefault(x => x.Type == ClaimTypes.GivenName); + if (givenName == null) + { + return UserName; + } + return givenName.Value; + } + } public string LastName => _claims?.FirstOrDefault(x => x.Type == ClaimTypes.Surname)?.Value!; diff --git a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs index ce9a9d60..7dd9b327 100644 --- a/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs +++ b/src/Infrastructure/BotSharp.Core/Users/Services/UserService.cs @@ -48,7 +48,7 @@ public class UserService : IUserService } record = user; - record.Email = user.Email.ToLower(); + record.Email = user.Email?.ToLower(); record.Salt = Guid.NewGuid().ToString("N"); record.Password = Utilities.HashText(user.Password, record.Salt); @@ -167,7 +167,7 @@ public class UserService : IUserService Issuer = issuer, Audience = audience, SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), - SecurityAlgorithms.HmacSha512Signature) + SecurityAlgorithms.HmacSha256Signature) }; var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.CreateToken(tokenDescriptor); @@ -178,7 +178,7 @@ public class UserService : IUserService public async Task GetMyProfile() { var db = _services.GetRequiredService(); - var user = db.GetUserById(_user.Id); + var user = db.GetUserByUserName(_user.UserName); return user; } diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj b/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj index 6ea73972..e1c23384 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharp.OpenAPI.csproj @@ -18,9 +18,18 @@ - - + + + + + + + + + + + diff --git a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs index 19584d5a..7d6796c5 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/BotSharpOpenApiExtensions.cs @@ -1,12 +1,14 @@ using BotSharp.Abstraction.Messaging.JsonConverters; using BotSharp.Core.Users.Services; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; +using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; using System.IdentityModel.Tokens.Jwt; namespace BotSharp.OpenAPI; @@ -29,11 +31,14 @@ public static class BotSharpOpenApiExtensions services.AddScoped(); // Add bearer authentication + var schema = "MIXED_SCHEME"; services.AddAuthentication(options => { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + // custom scheme defined in .AddPolicyScheme() below + // inspired from https://weblog.west-wind.com/posts/2022/Mar/29/Combining-Bearer-Token-and-Cookie-Auth-in-ASPNET + options.DefaultScheme = schema; + options.DefaultChallengeScheme = schema; + options.DefaultAuthenticateScheme = schema; }).AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters @@ -52,6 +57,32 @@ public static class BotSharpOpenApiExtensions o.TokenValidationParameters.SignatureValidator = (string token, TokenValidationParameters parameters) => new JwtSecurityToken(token); } + }).AddCookie(options => + { + }).AddGitHub(options => + { + options.ClientId = config["OAuth:GitHub:ClientId"]; + options.ClientSecret = config["OAuth:GitHub:ClientSecret"]; + options.Scope.Add("user:email"); + }).AddPolicyScheme(schema, "Mixed authentication", options => + { + // runs on each request + options.ForwardDefaultSelector = context => + { + // filter by auth type + string authorization = context.Request.Headers[HeaderNames.Authorization]; + if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer ")) + return JwtBearerDefaults.AuthenticationScheme; + else if (context.Request.Cookies.ContainsKey(".AspNetCore.Cookies")) + return CookieAuthenticationDefaults.AuthenticationScheme; + else if (context.Request.Path.StartsWithSegments("/sso") && context.Request.Method == "GET") + return CookieAuthenticationDefaults.AuthenticationScheme; + else if (context.Request.Path.ToString().StartsWith("/signin-") && context.Request.Method == "GET") + return CookieAuthenticationDefaults.AuthenticationScheme; + + // otherwise always check for cookie auth + return JwtBearerDefaults.AuthenticationScheme; + }; }); // Add services to the container. @@ -123,6 +154,7 @@ public static class BotSharpOpenApiExtensions if (env.IsDevelopment()) { + IdentityModelEventSource.ShowPII = true; app.UseSwaggerUI(); app.UseDeveloperExceptionPage(); } diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs index 13dd6c78..34ed7a0e 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/UserController.cs @@ -1,3 +1,6 @@ +using AspNet.Security.OAuth.GitHub; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using System.ComponentModel.DataAnnotations; namespace BotSharp.OpenAPI.Controllers; @@ -6,9 +9,11 @@ namespace BotSharp.OpenAPI.Controllers; [ApiController] public class UserController : ControllerBase { + private readonly IServiceProvider _services; private readonly IUserService _userService; - public UserController(IUserService userService) + public UserController(IUserService userService, IServiceProvider services) { + _services = services; _userService = userService; } @@ -30,6 +35,25 @@ public class UserController : ControllerBase return Ok(token); } + [AllowAnonymous] + [HttpGet("/sso/{provider}")] + public async Task Authorize([FromRoute] string provider) + { + return Challenge(new AuthenticationProperties { RedirectUri = $"page/user/me" }, provider); + } + + [AllowAnonymous] + [HttpGet("/signout")] + [HttpPost("/signout")] + public IActionResult SignOutCurrentUser() + { + // Instruct the cookies middleware to delete the local cookie created + // when the user agent is redirected from the external identity provider + // after a successful authentication flow (e.g Google or Facebook). + return SignOut(new AuthenticationProperties { RedirectUri = "/" }, + CookieAuthenticationDefaults.AuthenticationScheme); + } + [AllowAnonymous] [HttpPost("/user")] public async Task CreateUser(UserCreationModel user) @@ -38,10 +62,28 @@ public class UserController : ControllerBase return UserViewModel.FromUser(createdUser); } - [HttpGet("/user/my")] + [HttpGet("/user/me")] public async Task GetMyUserProfile() { var user = await _userService.GetMyProfile(); + if (user == null) + { + var identiy = _services.GetRequiredService(); + var accessor = _services.GetRequiredService(); + var claims = accessor.HttpContext.User.Claims; + if (claims.Any(x => x.Type == GitHubAuthenticationConstants.Claims.Name)) + { + user = await _userService.CreateUser(new User + { + Email = identiy.Email, + UserName = identiy.UserName, + FirstName = identiy.FirstName, + LastName = identiy.LastName, + Source = "GitHub", + ExternalId = identiy.Id, + }); + } + } return UserViewModel.FromUser(user); } } diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs index 2d7d2922..eeb1a8a2 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Users/UserViewModel.cs @@ -15,7 +15,8 @@ public class UserViewModel public string Email { get; set; } public string Role { get; set; } = UserRole.Client; [JsonPropertyName("full_name")] - public string FullName => $"{FirstName} {LastName}"; + public string FullName => $"{FirstName} {LastName}".Trim(); + public string Source { get; set; } [JsonPropertyName("external_id")] public string? ExternalId { get; set; } [JsonPropertyName("create_date")] @@ -43,6 +44,7 @@ public class UserViewModel LastName = user.LastName, Email = user.Email, Role = user.Role, + Source = user.Source, ExternalId = user.ExternalId, CreateDate = user.CreatedTime, UpdateDate = user.UpdatedTime diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj b/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj index 59c67182..43faa831 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj +++ b/src/Plugins/BotSharp.Plugin.ChatHub/BotSharp.Plugin.ChatHub.csproj @@ -16,10 +16,6 @@ \ - - - - diff --git a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs index c151a62e..bd3e90aa 100644 --- a/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs +++ b/src/Plugins/BotSharp.Plugin.ChatHub/WebSocketsMiddleware.cs @@ -20,7 +20,7 @@ public class WebSocketsMiddleware if (request.Path.StartsWithSegments("/chatHub", StringComparison.OrdinalIgnoreCase) && request.Query.TryGetValue("access_token", out var accessToken)) { - request.Headers.Add("Authorization", $"Bearer {accessToken}"); + request.Headers["Authorization"] = $"Bearer {accessToken}"; } await _next(httpContext); diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 426a141c..c5be322c 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -13,6 +13,13 @@ "Key": "31ba6052aa6f4569901facc3a41fcb4a" }, + "OAuth": { + "GitHub": { + "ClientId": "", + "ClientSecret": "" + } + }, + "LlmProviders": [ { "Provider": "azure-openai",