using System.Text.Json; using System.Text.Json.Serialization; using Hutopy.Infrastructure.Security; using Hutopy.Modules.Identity.Configuration; using Hutopy.Modules.Identity.Data; using Microsoft.Extensions.Options; namespace Hutopy.Modules.Identity.Handlers; 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) : Endpoint { 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) { var googleToken = JsonSerializer.Deserialize(request.Token)!; // Verify the token with Google using var httpClient = httpClientFactory.CreateClient(); using var 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.). var content = await response.Content.ReadAsStringAsync(ct); var userInfo = JsonSerializer.Deserialize(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 user exists or create a new one var user = await userManager.FindByEmailAsync(userInfo.Email); if (user is null) { var generatedPassword = PasswordGenerator.Next(); var refreshToken = RefreshTokenGenerator.Next(); var generatedUser = new User { UserName = userInfo.Email, Email = userInfo.Email, Firstname = userInfo.GivenName, Lastname = userInfo.FamilyName, Alias = userInfo.Name, PortraitUrl = userInfo.Picture, GoogleId = userInfo.Id, RefreshToken = refreshToken, RefreshTokenExpiryTime = DateTime.UtcNow.Add(jwtOptions.Value.RefreshTokenLifetime) }; var result = await userManager.CreateAsync( generatedUser, generatedPassword); if (!result.Succeeded) { await SendStringAsync( result.Errors.First().Description, 400, cancellation: ct); return; } user = generatedUser; } // Generate new refresh token user.RefreshToken = RefreshTokenGenerator.Next(); user.RefreshTokenExpiryTime = DateTime.UtcNow.Add(jwtOptions.Value.RefreshTokenLifetime); await userManager.UpdateAsync(user); var accessToken = JwtTokenHelper.GenerateJwtToken( expiresIn: jwtOptions.Value.Lifetime, issuer: jwtOptions.Value.Issuer, audience: jwtOptions.Value.Audience, key: jwtOptions.Value.Key, userId: user.Id.ToString(), email: user.Email ?? string.Empty, alias: user.Alias, firstname: user.Firstname ?? string.Empty, lastname: user.Lastname ?? string.Empty, portraitUrl: user.PortraitUrl); await SendOkAsync( new LoginWithGoogleResponse(accessToken, user.RefreshToken), cancellation: ct); } }