Implement Google authentication (for Web) and user management system

This commit is contained in:
Michał Zieliński
2025-07-19 22:50:38 +02:00
parent b673fd2da3
commit 14c61ca1ee
25 changed files with 1072 additions and 41 deletions

View File

@@ -7,16 +7,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Apis.Auth" Version="1.70.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.17">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Bimix.Domain\Bimix.Domain.csproj" />
<ProjectReference Include="..\Bimix.Application\Bimix.Application.csproj" />
<ProjectReference Include="..\Bimix.Infrastructure\Bimix.Infrastructure.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,96 @@
using System.Security.Claims;
using Bimix.API.Services;
using Bimix.Application.DTOModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bimix.API.Controllers;
public class AuthController(
GoogleAuthService googleAuthService,
JwtTokenService jwtTokenService,
ILogger<AuthController> logger)
: ControllerBase
{
[HttpPost("google")]
public async Task<IActionResult> GoogleAuth([FromBody] GoogleAuthRequest request)
{
try
{
if (string.IsNullOrEmpty(request.IdToken))
{
return BadRequest(new GoogleAuthResponse
{
Success = false,
Error = "IdToken is required"
});
}
var (isValid, user, error) = await googleAuthService.ValidateGoogleTokenAsync(request.IdToken);
if (!isValid || user == null)
{
var statusCode = error switch
{
"User not authorized to access this application" => 403,
"User account is not active" => 403,
"Invalid Google token" => 401,
_ => 401
};
return StatusCode(statusCode, new GoogleAuthResponse
{
Success = false,
Error = error ?? "Authentication failed"
});
}
var jwt = jwtTokenService.GenerateToken(user);
return Ok(new GoogleAuthResponse
{
Success = true,
Token = jwt,
User = new UserDto
{
Id = user.Id,
Email = user.Email,
FullName = user.FullName,
IsActive = user.IsActive,
LastLoginAt = user.LastLoginAt
}
});
}
catch (Exception ex)
{
logger.LogError(ex, "Error during Google authentication");
return StatusCode(500, new GoogleAuthResponse
{
Success = false,
Error = "Internal server error"
});
}
}
[HttpGet("me")]
[Authorize]
public IActionResult GetCurrentUser()
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier);
var emailClaim = User.FindFirst(ClaimTypes.Email);
var nameClaim = User.FindFirst(ClaimTypes.Name);
if (userIdClaim == null || emailClaim == null || nameClaim == null)
{
return Unauthorized();
}
return Ok(new UserDto
{
Id = Guid.Parse(userIdClaim.Value),
Email = emailClaim.Value,
FullName = nameClaim.Value,
IsActive = true,
});
}
}

View File

@@ -1,6 +1,11 @@
using System.Text;
using Bimix.API.Services;
using Bimix.Infrastructure.Data;
using Bimix.Infrastructure.Sync;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
@@ -13,6 +18,43 @@ builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Start auth section
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var secretKey = jwtSettings["SecretKey"];
var issuer = jwtSettings["Issuer"];
var audience = jwtSettings["Audience"];
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = issuer,
ValidAudience = audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
ClockSkew = TimeSpan.Zero,
};
});
builder.Services.AddAuthentication();
builder.Services.AddScoped<GoogleAuthService>();
builder.Services.AddScoped<JwtTokenService>();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
// End auth section
var app = builder.Build();
if (app.Environment.IsDevelopment())
@@ -22,6 +64,8 @@ if (app.Environment.IsDevelopment())
}
app.UseHttpsRedirection();
app.UseCors("AllowAll");
app.UseAuthorization();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,72 @@
using Bimix.Domain.Entities;
using Bimix.Infrastructure.Data;
using Google.Apis.Auth;
using Microsoft.EntityFrameworkCore;
namespace Bimix.API.Services;
public class GoogleAuthService(BimixDbContext context, IConfiguration configuration, ILogger<GoogleAuthService> logger)
{
private readonly BimixDbContext _context = context;
private readonly IConfiguration _configuration = configuration;
private readonly ILogger<GoogleAuthService> _logger = logger;
public async Task<(bool IsValid, User? user, string? error)> ValidateGoogleTokenAsync(string idToken)
{
try
{
var clientId = _configuration["GoogleAuth:ClientId"];
if (string.IsNullOrEmpty(clientId))
{
_logger.LogError("Google Auth Client Id is not configured");
return (false, null, "Google Auth Client Id is not configured");
}
var payload = await GoogleJsonWebSignature.ValidateAsync(idToken,
new GoogleJsonWebSignature.ValidationSettings
{
Audience = new[] { clientId }
});
_logger.LogInformation("Google token validated for user: {Email}", payload.Email);
var user = await _context.Users
.FirstOrDefaultAsync(x => x.GoogleId == payload.Subject || x.Email == payload.Email);
if (user == null)
{
_logger.LogError("User not found in Bimix database: {Email}", payload.Email);
return (false, null, "User not found in Bimix database");
}
if (!user.IsActive)
{
_logger.LogError("User is not active: {Email}", payload.Email);
return (false, null, "User is not active");
}
user.LastLoginAt = DateTime.UtcNow;
user.FullName = payload.Name;
if (user.GoogleId != payload.Subject)
{
user.GoogleId = payload.Subject;
}
await _context.SaveChangesAsync();
_logger.LogInformation("User logged in: {Email}", payload.Email);
return (true, user, null);
}
catch (InvalidJwtException ex)
{
_logger.LogError(ex, "Invalid JWT token");
return (false, null, "Invalid JWT token");
} catch (Exception ex)
{
_logger.LogError(ex, "Error validating Google token");
return (false, null, "Error validating Google token");
}
}
}

View File

@@ -0,0 +1,87 @@
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Bimix.Domain.Entities;
namespace Bimix.API.Services;
public class JwtTokenService(IConfiguration configuration, ILogger<JwtTokenService> logger)
{
private readonly IConfiguration _configuration = configuration;
private readonly ILogger<JwtTokenService> _logger = logger;
public string GenerateToken(User user)
{
var jwtSettings = _configuration.GetSection("JwtSettings");
var securityKey = jwtSettings["SecurityKey"];
var issuer = jwtSettings["Issuer"];
var audience = jwtSettings["Audience"];
var expiryDays = int.Parse(jwtSettings["ExpiryDays"] ?? "7");
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Name, user.FullName),
new Claim("google_id", user.GoogleId),
new Claim("is_active", user.IsActive.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString(),
ClaimValueTypes.Integer64)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: issuer,
audience: audience,
claims: claims,
expires: DateTime.UtcNow.AddDays(expiryDays),
signingCredentials: creds
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
_logger.LogInformation("Generated JWT token for user: {Email}", user.Email);
return tokenString;
}
public ClaimsPrincipal? ValidateToken(string token)
{
try
{
var jwtSettings = _configuration.GetSection("JwtSettings");
var secretKey = jwtSettings["SecretKey"];
var issuer = jwtSettings["Issuer"];
var audience = jwtSettings["Audience"];
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(secretKey);
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = issuer,
ValidAudience = audience,
IssuerSigningKey = new SymmetricSecurityKey(key),
ClockSkew = TimeSpan.Zero
};
var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
return principal;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating JWT token");
return null;
}
}
}