GitHub SSO

This commit is contained in:
Haiping Chen 2024-02-14 17:45:28 -06:00
parent 596a83655e
commit 8f2ef5ba63
9 changed files with 119 additions and 21 deletions

View file

@ -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!;

View file

@ -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;
}

View file

@ -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>

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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" />

View file

@ -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);

View file

@ -13,6 +13,13 @@
"Key": "31ba6052aa6f4569901facc3a41fcb4a"
},
"OAuth": {
"GitHub": {
"ClientId": "",
"ClientSecret": ""
}
},
"LlmProviders": [
{
"Provider": "azure-openai",