Feature: Google oauth
This commit is contained in:
@@ -32,7 +32,7 @@ public class UserService(UserManager<ApplicationUser> userManager) : IUserServic
|
||||
|
||||
public async Task CreateUserAsync(Userinfo userInfo)
|
||||
{
|
||||
await CreateUserAsync(userInfo.Email, userInfo.GivenName, userInfo.GivenName, userInfo.FamilyName, GeneratePassword(24));
|
||||
await CreateUserAsync(userInfo.Email, userInfo.GivenName, userInfo.GivenName, userInfo.FamilyName, RandomGenerator.RandomString(24));
|
||||
}
|
||||
|
||||
public async Task<UserModel?> FindUserByIdAsync(string id)
|
||||
@@ -82,23 +82,55 @@ public class UserService(UserManager<ApplicationUser> userManager) : IUserServic
|
||||
|
||||
return userModel;
|
||||
}
|
||||
|
||||
private const string Characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
private const string SpecialCharacters = "!@#$%^&*()_+";
|
||||
|
||||
private String GeneratePassword(int length)
|
||||
{
|
||||
// Using a string builder has additional overhead, maybe we can find something else
|
||||
var password = new StringBuilder();
|
||||
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
password.Append(Characters[_random.Next(Characters.Length)]);
|
||||
}
|
||||
|
||||
password.Append(SpecialCharacters[_random.Next(SpecialCharacters.Length)]);
|
||||
|
||||
return password.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class RandomGenerator
|
||||
{
|
||||
private const string LetterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
+ "0123456789"
|
||||
+ "!@#$%^&*()_+"
|
||||
+ "-=[];',./`~{}|:\"<>?";
|
||||
private const int LetterIdxBits = 6;
|
||||
private const int LetterIdxMask = 1 << LetterIdxBits;
|
||||
private const int LetterIdxMax = 64 / LetterIdxBits;
|
||||
|
||||
private static readonly Random Src = new();
|
||||
|
||||
public static byte[] RandBytesMaskSrc(int n)
|
||||
{
|
||||
var b = new byte[n];
|
||||
|
||||
for (var i = n - 1; i >= 0;)
|
||||
{
|
||||
long cache = Src.NextInt64();
|
||||
int remain = LetterIdxMax;
|
||||
|
||||
while (remain != 0)
|
||||
{
|
||||
if (i < 0)
|
||||
break;
|
||||
|
||||
if (cache == 0)
|
||||
cache = Src.NextInt64();
|
||||
|
||||
var idx = (int)(cache & LetterIdxMask);
|
||||
if (idx < LetterBytes.Length)
|
||||
{
|
||||
b[i] = (byte)LetterBytes[idx];
|
||||
i--;
|
||||
}
|
||||
|
||||
cache >>= LetterIdxBits;
|
||||
remain--;
|
||||
}
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
public static string RandomString(int length)
|
||||
{
|
||||
var bytes = RandBytesMaskSrc(length);
|
||||
return Encoding.UTF8.GetString(bytes); // Equivalent for *(string*)(&bytes[0])
|
||||
}
|
||||
}
|
||||
|
||||
54
src/Web/Controllers/GoogleController.cs
Normal file
54
src/Web/Controllers/GoogleController.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Security.Claims;
|
||||
using Google.Apis.Oauth2.v2.Data;
|
||||
using Hutopy.Domain.Interfaces;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.Google;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Hutopy.Web.Controllers;
|
||||
|
||||
public class GoogleController(
|
||||
IUserService userService) : Controller
|
||||
{
|
||||
[HttpGet("/api/google/sign-in")]
|
||||
public async Task SignIn()
|
||||
{
|
||||
await HttpContext.ChallengeAsync(GoogleDefaults.AuthenticationScheme, new AuthenticationProperties
|
||||
{
|
||||
RedirectUri = Url.Action("Callback"),
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Callback()
|
||||
{
|
||||
var authenticateResult = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
|
||||
if (!authenticateResult.Succeeded)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var claims = authenticateResult.Principal.Claims.ToList();
|
||||
|
||||
var userInfo = new Userinfo
|
||||
{
|
||||
Name = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value,
|
||||
Email = claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value,
|
||||
GivenName = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName)?.Value,
|
||||
FamilyName = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname)?.Value
|
||||
};
|
||||
|
||||
await userService.CreateUserAsync(userInfo); // TODO: Don't create user if already exists
|
||||
|
||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.Name, userInfo.Name),
|
||||
new(ClaimTypes.Email, userInfo.Email),
|
||||
new(ClaimTypes.GivenName, userInfo.GivenName),
|
||||
new(ClaimTypes.Surname, userInfo.FamilyName)
|
||||
}, CookieAuthenticationDefaults.AuthenticationScheme)));
|
||||
|
||||
return Redirect("/");
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Google;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Hutopy.Web.Endpoints;
|
||||
|
||||
public class Google : EndpointGroupBase
|
||||
{
|
||||
public override void Map(WebApplication app)
|
||||
{
|
||||
app.MapGroup(this)
|
||||
.MapGet("/o/sign-in", Callback);
|
||||
}
|
||||
|
||||
private static async Task<IActionResult> Callback(ISender sender, HttpContext context)
|
||||
{
|
||||
var properties = new AuthenticationProperties
|
||||
{
|
||||
RedirectUri = "/signin-google", ExpiresUtc = DateTimeOffset.UtcNow.AddDays(30),
|
||||
};
|
||||
|
||||
await context.ChallengeAsync(GoogleDefaults.AuthenticationScheme, properties);
|
||||
|
||||
return new ChallengeResult(GoogleDefaults.AuthenticationScheme, properties);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,11 @@ using Hutopy.Infrastructure.Data;
|
||||
using Hutopy.Infrastructure.Services;
|
||||
using Hutopy.Web;
|
||||
using Azure.Identity;
|
||||
using Hutopy.Infrastructure.Identity;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.Google;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -49,40 +53,43 @@ builder.Services.AddInfrastructureServices(builder.Configuration);
|
||||
builder.Services.AddWebServices();
|
||||
|
||||
// OAuth
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
options.Cookie.Name = "Hutopy";
|
||||
options.Cookie.SecurePolicy =
|
||||
builder.Environment.IsDevelopment() ? CookieSecurePolicy.None : CookieSecurePolicy.Always;
|
||||
options.Cookie.SameSite = SameSiteMode.Strict;
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.IsEssential = true;
|
||||
options.Cookie.MaxAge = TimeSpan.FromDays(30);
|
||||
})
|
||||
.AddGoogle(options =>
|
||||
.AddCookie()
|
||||
.AddGoogle(
|
||||
GoogleDefaults.AuthenticationScheme,
|
||||
options =>
|
||||
{
|
||||
options.ClientId = builder.Configuration["Google:ClientId"] ??
|
||||
throw new ArgumentNullException("The Google ClientId is missing.");
|
||||
options.ClientSecret = builder.Configuration["Google:ClientSecret"] ??
|
||||
throw new ArgumentNullException("The Google ClientSecret is missing.");
|
||||
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
options.Events.OnRedirectToAuthorizationEndpoint = context =>
|
||||
{
|
||||
context.Response.Redirect(context.RedirectUri + "&prompt=consent");
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
});
|
||||
|
||||
// Password hashing
|
||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = true;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireUppercase = true;
|
||||
options.Password.RequireNonAlphanumeric = true;
|
||||
options.Password.RequiredLength = 8;
|
||||
})
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseForwardedHeaders(
|
||||
new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto }
|
||||
);
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
|
||||
@@ -26,27 +26,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/Google/o/sign-in": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Google"
|
||||
],
|
||||
"operationId": "GetApiGoogleOSignIn",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/JoinUs": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -628,6 +607,19 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/google/sign-in": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Google"
|
||||
],
|
||||
"operationId": "Google_SignIn",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
||||
Reference in New Issue
Block a user