140 lines
5.0 KiB
C#
140 lines
5.0 KiB
C#
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Socialize.Infrastructure.Security;
|
|
using Socialize.Modules.Identity.Configuration;
|
|
using Socialize.Modules.Identity.Data;
|
|
using Socialize.Modules.Identity.Services;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Socialize.Modules.Identity.Handlers;
|
|
|
|
internal class GoogleToken
|
|
{
|
|
[JsonPropertyName("access_token")] public required string AccessToken { get; init; }
|
|
[JsonPropertyName("token_type")] public required string TokenType { get; init; }
|
|
[JsonPropertyName("expires_in")] public required int ExpiresIn { get; init; }
|
|
[JsonPropertyName("scope")] public required string Scope { get; init; }
|
|
[JsonPropertyName("authuser")] public required string AuthUser { get; init; }
|
|
[JsonPropertyName("prompt")] public required string Prompt { get; init; }
|
|
}
|
|
|
|
public class GoogleUserInfo
|
|
{
|
|
[JsonPropertyName("id")] public required string Id { get; init; }
|
|
[JsonPropertyName("email")] public required string Email { get; init; }
|
|
[JsonPropertyName("verified_email")] public required bool VerifiedEmail { get; init; }
|
|
[JsonPropertyName("name")] public required string Name { get; init; }
|
|
[JsonPropertyName("given_name")] public required string GivenName { get; init; }
|
|
[JsonPropertyName("family_name")] public string FamilyName { get; init; } = string.Empty;
|
|
[JsonPropertyName("picture")] public required string Picture { get; init; }
|
|
}
|
|
|
|
[PublicAPI]
|
|
public record LoginWithGoogleRequest(
|
|
string Token);
|
|
|
|
[PublicAPI]
|
|
public record LoginWithGoogleResponse(
|
|
string AccessToken,
|
|
string RefreshToken);
|
|
|
|
[PublicAPI]
|
|
public class LoginWithGoogleHandler(
|
|
IHttpClientFactory httpClientFactory,
|
|
UserManager userManager,
|
|
IOptionsSnapshot<JwtOptions> jwtOptions,
|
|
AccessTokenFactory accessTokenFactory)
|
|
: Endpoint<LoginWithGoogleRequest, LoginWithGoogleResponse>
|
|
{
|
|
public override void Configure()
|
|
{
|
|
AllowAnonymous();
|
|
Post("/api/users/login-with-google");
|
|
Options(o => o.WithTags("Users"));
|
|
}
|
|
|
|
public override async Task HandleAsync(
|
|
LoginWithGoogleRequest request,
|
|
CancellationToken ct)
|
|
{
|
|
GoogleToken googleToken = JsonSerializer.Deserialize<GoogleToken>(request.Token)!;
|
|
|
|
// Verify the token with Google
|
|
using HttpClient httpClient = httpClientFactory.CreateClient();
|
|
using HttpResponseMessage response = await httpClient.GetAsync(
|
|
$"https://www.googleapis.com/oauth2/v1/userinfo?access_token={googleToken.AccessToken}",
|
|
ct);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
await SendStringAsync(
|
|
"The token is not valid",
|
|
400,
|
|
cancellation: ct);
|
|
return;
|
|
}
|
|
|
|
// Extract the user info (email, name, etc.).
|
|
string content = await response.Content.ReadAsStringAsync(ct);
|
|
GoogleUserInfo? userInfo = JsonSerializer.Deserialize<GoogleUserInfo>(content);
|
|
if (userInfo is null
|
|
|| !userInfo.VerifiedEmail
|
|
|| string.IsNullOrEmpty(userInfo.Email))
|
|
{
|
|
await SendStringAsync(
|
|
"The token does not contain an email",
|
|
400,
|
|
cancellation: ct);
|
|
return;
|
|
}
|
|
|
|
// Check if the user exists or create a new one
|
|
User? user = await userManager.FindByEmailAsync(userInfo.Email);
|
|
|
|
if (user is null)
|
|
{
|
|
string generatedPassword = PasswordGenerator.Next();
|
|
string refreshToken = RefreshTokenGenerator.Next();
|
|
User generatedUser = new()
|
|
{
|
|
UserName = userInfo.Email,
|
|
Email = userInfo.Email,
|
|
EmailConfirmed = true,
|
|
Firstname = userInfo.GivenName,
|
|
Lastname = userInfo.FamilyName,
|
|
Alias = userInfo.Name,
|
|
PortraitUrl = userInfo.Picture,
|
|
GoogleId = userInfo.Id,
|
|
RefreshToken = refreshToken,
|
|
RefreshTokenExpiryTime = DateTime.UtcNow.Add(jwtOptions.Value.RefreshTokenLifetime)
|
|
};
|
|
|
|
IdentityResult result = await userManager.CreateAsync(
|
|
generatedUser,
|
|
generatedPassword);
|
|
|
|
if (!result.Succeeded)
|
|
{
|
|
await SendStringAsync(
|
|
result.Errors.First().Description,
|
|
400,
|
|
cancellation: ct);
|
|
return;
|
|
}
|
|
|
|
user = generatedUser;
|
|
}
|
|
|
|
// Generate the new refresh token
|
|
user.RefreshToken = RefreshTokenGenerator.Next();
|
|
user.RefreshTokenExpiryTime = DateTime.UtcNow.Add(jwtOptions.Value.RefreshTokenLifetime);
|
|
await userManager.UpdateAsync(user);
|
|
|
|
string accessToken = await accessTokenFactory.CreateAsync(user);
|
|
|
|
await SendOkAsync(
|
|
new LoginWithGoogleResponse(accessToken, user.RefreshToken),
|
|
ct);
|
|
}
|
|
}
|