Files
pas/Api/Controllers/AuthController.cs
Marek Lesko f34d523413 feat: Implement OAuth2 authentication with Microsoft, Google, and PocketId
- Added JWT configuration to appsettings.json for secure token handling.
- Updated config.json to include OAuth provider details for Microsoft, Google, and PocketId.
- Added Microsoft icon SVG for UI representation.
- Refactored app.config.ts to use a custom AuthInterceptor for managing access tokens.
- Enhanced auth route guard to handle asynchronous authentication checks.
- Created new auth models for structured request and response handling.
- Developed a callback component to manage user login states and transitions.
- Updated side-login component to support multiple OAuth providers with loading states.
- Implemented authentication service methods for handling OAuth login flows and token management.
- Added error handling and user feedback for authentication processes.
2025-11-07 19:23:21 +00:00

273 lines
11 KiB
C#

using Api.Models;
using Api.Models.DTOs;
using Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly AppDbContext _context;
private readonly IOAuthValidationService _oauthValidationService;
private readonly IJwtService _jwtService;
private readonly ILogger<AuthController> _logger;
public AuthController(
AppDbContext context,
IOAuthValidationService oauthValidationService,
IJwtService jwtService,
ILogger<AuthController> logger)
{
_context = context;
_oauthValidationService = oauthValidationService;
_jwtService = jwtService;
_logger = logger;
}
/// <summary>
/// Authenticates a user with an OAuth ID token and returns a custom access token
/// </summary>
/// <param name="request">Authentication request containing ID token and provider</param>
/// <returns>Custom access token and user information</returns>
[HttpPost("authenticate")]
public async Task<ActionResult<AuthenticateResponse>> AuthenticateAsync([FromBody] AuthenticateRequest request)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
try
{
// Validate the ID token with the specified provider
var (isValid, principal, errorMessage) = await _oauthValidationService
.ValidateIdTokenAsync(request.IdToken, request.Provider);
if (!isValid || principal == null)
{
_logger.LogWarning("Invalid ID token for provider {Provider}: {Error}", request.Provider, errorMessage);
return BadRequest(new { error = "invalid_token", message = errorMessage ?? "Invalid ID token" });
}
// Extract user information from the validated token
var userInfo = request.Provider.ToLowerInvariant() == "microsoft"
? await _oauthValidationService.ExtractUserInfoAsync(principal, request.Provider, request.IdToken, request.AccessToken)
: _oauthValidationService.ExtractUserInfo(principal, request.Provider);
if (string.IsNullOrEmpty(userInfo.Email))
{
_logger.LogWarning("No email found in {Provider} token", request.Provider);
return BadRequest(new { error = "invalid_token", message = "Email claim is required" });
}
// Parse the provider enum
if (!Enum.TryParse<OAuthProvider>(request.Provider, true, out var providerEnum))
{
return BadRequest(new { error = "invalid_provider", message = $"Unsupported provider: {request.Provider}" });
}
// Find or create user
var (user, isNewUser) = await FindOrCreateUserAsync(userInfo, providerEnum);
// Update last login time
user.LastLoginAt = DateTime.UtcNow;
// Update or create OAuth provider record
await UpdateUserOAuthProviderAsync(user, userInfo, providerEnum);
await _context.SaveChangesAsync();
// Generate custom access token
var accessToken = _jwtService.GenerateToken(user);
var expiresAt = _jwtService.GetTokenExpiration(accessToken);
// Prepare response
var userProfile = new UserProfile
{
Id = user.Id,
Email = user.Email,
FirstName = user.FirstName,
LastName = user.LastName,
ProfilePictureUrl = user.ProfilePictureUrl,
CreatedAt = user.CreatedAt,
LastLoginAt = user.LastLoginAt,
Providers = user.OAuthProviders.Select(p => p.Provider.ToString()).ToList()
};
var response = new AuthenticateResponse
{
AccessToken = accessToken,
ExpiresAt = expiresAt,
User = userProfile,
IsNewUser = isNewUser
};
_logger.LogInformation("User {Email} authenticated successfully with {Provider} (New: {IsNew})",
user.Email, request.Provider, isNewUser);
return Ok(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Authentication failed for provider {Provider}", request.Provider);
return StatusCode(500, new { error = "internal_error", message = "Authentication failed" });
}
}
/// <summary>
/// Gets the current user's profile information
/// </summary>
/// <returns>User profile information</returns>
[HttpGet("me")]
[Authorize]
public async Task<ActionResult<UserProfile>> GetCurrentUserAsync()
{
try
{
var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
if (userIdClaim == null || !int.TryParse(userIdClaim.Value, out var userId))
{
return Unauthorized(new { error = "invalid_token", message = "User ID not found in token" });
}
var user = await _context.Users
.Include(u => u.OAuthProviders)
.FirstOrDefaultAsync(u => u.Id == userId && u.IsActive);
if (user == null)
{
return NotFound(new { error = "user_not_found", message = "User not found or inactive" });
}
var userProfile = new UserProfile
{
Id = user.Id,
Email = user.Email,
FirstName = user.FirstName,
LastName = user.LastName,
ProfilePictureUrl = user.ProfilePictureUrl,
CreatedAt = user.CreatedAt,
LastLoginAt = user.LastLoginAt,
Providers = user.OAuthProviders.Select(p => p.Provider.ToString()).ToList()
};
return Ok(userProfile);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving current user profile");
return StatusCode(500, new { error = "internal_error", message = "Failed to retrieve user profile" });
}
}
/// <summary>
/// Revokes the current access token (logout)
/// </summary>
/// <returns>Success message</returns>
[HttpPost("logout")]
[Authorize]
public IActionResult Logout()
{
// In a real application, you might want to maintain a blacklist of revoked tokens
// For now, we'll just return success as JWT tokens are stateless
_logger.LogInformation("User logged out");
return Ok(new { message = "Logged out successfully" });
}
private async Task<(User User, bool IsNewUser)> FindOrCreateUserAsync(
(string Email, string? FirstName, string? LastName, string? ProfilePictureUrl, string ProviderId) userInfo,
OAuthProvider provider)
{
var existingUser = await _context.Users
.Include(u => u.OAuthProviders)
.FirstOrDefaultAsync(u => u.Email == userInfo.Email && u.IsActive);
if (existingUser != null)
{
// Update user information if it has changed
var hasChanges = false;
if (!string.IsNullOrEmpty(userInfo.FirstName) && existingUser.FirstName != userInfo.FirstName)
{
existingUser.FirstName = userInfo.FirstName;
hasChanges = true;
}
if (!string.IsNullOrEmpty(userInfo.LastName) && existingUser.LastName != userInfo.LastName)
{
existingUser.LastName = userInfo.LastName;
hasChanges = true;
}
if (!string.IsNullOrEmpty(userInfo.ProfilePictureUrl) && existingUser.ProfilePictureUrl != userInfo.ProfilePictureUrl)
{
existingUser.ProfilePictureUrl = userInfo.ProfilePictureUrl;
hasChanges = true;
}
if (hasChanges)
{
_logger.LogInformation("Updated user information for {Email}", userInfo.Email);
}
return (existingUser, false);
}
// Create new user
var newUser = new User
{
Email = userInfo.Email,
FirstName = userInfo.FirstName,
LastName = userInfo.LastName,
ProfilePictureUrl = userInfo.ProfilePictureUrl,
CreatedAt = DateTime.UtcNow,
IsActive = true
};
_context.Users.Add(newUser);
_logger.LogInformation("Created new user for {Email}", userInfo.Email);
return (newUser, true);
}
private async Task UpdateUserOAuthProviderAsync(
User user,
(string Email, string? FirstName, string? LastName, string? ProfilePictureUrl, string ProviderId) userInfo,
OAuthProvider provider)
{
var existingProvider = user.OAuthProviders
.FirstOrDefault(p => p.Provider == provider);
if (existingProvider != null)
{
// Update existing provider record
existingProvider.ProviderEmail = userInfo.Email;
existingProvider.ProviderName = $"{userInfo.FirstName} {userInfo.LastName}".Trim();
existingProvider.LastUsedAt = DateTime.UtcNow;
}
else
{
// Create new provider record
var newProvider = new UserOAuthProvider
{
UserId = user.Id,
Provider = provider,
ProviderId = userInfo.ProviderId,
ProviderEmail = userInfo.Email,
ProviderName = $"{userInfo.FirstName} {userInfo.LastName}".Trim(),
CreatedAt = DateTime.UtcNow,
LastUsedAt = DateTime.UtcNow
};
user.OAuthProviders.Add(newProvider);
_logger.LogInformation("Added {Provider} OAuth provider for user {Email}", provider, user.Email);
}
await Task.CompletedTask; // Make the method actually async
}
}
}