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)
|
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)
|
public async Task<UserModel?> FindUserByIdAsync(string id)
|
||||||
@@ -82,23 +82,55 @@ public class UserService(UserManager<ApplicationUser> userManager) : IUserServic
|
|||||||
|
|
||||||
return userModel;
|
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.Infrastructure.Services;
|
||||||
using Hutopy.Web;
|
using Hutopy.Web;
|
||||||
using Azure.Identity;
|
using Azure.Identity;
|
||||||
|
using Hutopy.Infrastructure.Identity;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Google;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -49,40 +53,43 @@ builder.Services.AddInfrastructureServices(builder.Configuration);
|
|||||||
builder.Services.AddWebServices();
|
builder.Services.AddWebServices();
|
||||||
|
|
||||||
// OAuth
|
// OAuth
|
||||||
builder.Services.AddAuthorization();
|
|
||||||
builder.Services.AddAuthentication(options =>
|
builder.Services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||||
|
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
|
||||||
})
|
})
|
||||||
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
|
.AddCookie()
|
||||||
{
|
.AddGoogle(
|
||||||
options.Cookie.Name = "Hutopy";
|
GoogleDefaults.AuthenticationScheme,
|
||||||
options.Cookie.SecurePolicy =
|
options =>
|
||||||
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 =>
|
|
||||||
{
|
{
|
||||||
options.ClientId = builder.Configuration["Google:ClientId"] ??
|
options.ClientId = builder.Configuration["Google:ClientId"] ??
|
||||||
throw new ArgumentNullException("The Google ClientId is missing.");
|
throw new ArgumentNullException("The Google ClientId is missing.");
|
||||||
options.ClientSecret = builder.Configuration["Google:ClientSecret"] ??
|
options.ClientSecret = builder.Configuration["Google:ClientSecret"] ??
|
||||||
throw new ArgumentNullException("The Google ClientSecret is missing.");
|
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.AddControllers();
|
||||||
builder.Services.AddScoped<IUserService, UserService>();
|
builder.Services.AddScoped<IUserService, UserService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseForwardedHeaders(
|
||||||
|
new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto }
|
||||||
|
);
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
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": {
|
"/api/JoinUs": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -628,6 +607,19 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/google/sign-in": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Google"
|
||||||
|
],
|
||||||
|
"operationId": "Google_SignIn",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
|||||||
Reference in New Issue
Block a user