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, AccessTokenFactory accessTokenFactory) : 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) { GoogleToken googleToken = JsonSerializer.Deserialize(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(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); } }