GitHub SSO
This commit is contained in:
parent
596a83655e
commit
8f2ef5ba63
|
|
@ -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!;
|
||||
|
|
|
|||
|
|
@ -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<User> GetMyProfile()
|
||||
{
|
||||
var db = _services.GetRequiredService<IBotSharpRepository>();
|
||||
var user = db.GetUserById(_user.Id);
|
||||
var user = db.GetUserByUserName(_user.UserName);
|
||||
return user;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,18 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="8.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
|
||||
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="6.0.15" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.26" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IUserIdentity, UserIdentity>();
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<IActionResult> 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<UserViewModel> CreateUser(UserCreationModel user)
|
||||
|
|
@ -38,10 +62,28 @@ public class UserController : ControllerBase
|
|||
return UserViewModel.FromUser(createdUser);
|
||||
}
|
||||
|
||||
[HttpGet("/user/my")]
|
||||
[HttpGet("/user/me")]
|
||||
public async Task<UserViewModel> GetMyUserProfile()
|
||||
{
|
||||
var user = await _userService.GetMyProfile();
|
||||
if (user == null)
|
||||
{
|
||||
var identiy = _services.GetRequiredService<IUserIdentity>();
|
||||
var accessor = _services.GetRequiredService<IHttpContextAccessor>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -16,10 +16,6 @@
|
|||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Infrastructure\BotSharp.OpenAPI\BotSharp.OpenAPI.csproj" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@
|
|||
"Key": "31ba6052aa6f4569901facc3a41fcb4a"
|
||||
},
|
||||
|
||||
"OAuth": {
|
||||
"GitHub": {
|
||||
"ClientId": "",
|
||||
"ClientSecret": ""
|
||||
}
|
||||
},
|
||||
|
||||
"LlmProviders": [
|
||||
{
|
||||
"Provider": "azure-openai",
|
||||
|
|
|
|||
Loading…
Reference in a new issue