diff --git a/.dockerignore b/.dockerignore index 3729ff0cd1acce411e814ec416b82ac0f239a4e0..76f7f241dd531c91f3f63e78955e378e01b9712a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -21,5 +21,6 @@ **/obj **/secrets.dev.yaml **/values.dev.yaml +appsettings.Development.json LICENSE README.md \ No newline at end of file diff --git a/AuthenticationMicroservice.csproj b/AuthenticationMicroservice.csproj index d57bb8af427b8a388804b9ebc91ac6c0bde844f5..8c6b137d42d8b37ef2005930dd4334a84f2d06b4 100644 --- a/AuthenticationMicroservice.csproj +++ b/AuthenticationMicroservice.csproj @@ -10,35 +10,35 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="AutoMapper" Version="12.0.1" /> - <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" /> - <PackageReference Include="Azure.Storage.Blobs" Version="12.16.0-beta.1" /> - <PackageReference Include="Azure.Storage.Common" Version="12.15.0-beta.1" /> - <PackageReference Include="BenchmarkDotNet" Version="0.13.5" /> - <PackageReference Include="LazyCache" Version="2.4.0" /> - <PackageReference Include="LazyCache.AspNetCore" Version="2.4.0" /> - <PackageReference Include="MailKit" Version="3.6.0" /> - <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.5" /> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="7.0.5" /> - <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" /> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.5" /> + <PackageReference Include="AutoMapper" Version="12.0.1"/> + <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0"/> + <PackageReference Include="Azure.Storage.Blobs" Version="12.16.0-beta.1"/> + <PackageReference Include="Azure.Storage.Common" Version="12.15.0-beta.1"/> + <PackageReference Include="BenchmarkDotNet" Version="0.13.5"/> + <PackageReference Include="LazyCache" Version="2.4.0"/> + <PackageReference Include="LazyCache.AspNetCore" Version="2.4.0"/> + <PackageReference Include="MailKit" Version="3.6.0"/> + <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.5"/> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="7.0.5"/> + <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5"/> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.5"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.5" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.5" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.5"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.5"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.5"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" /> - <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> - <PackageReference Include="Polly" Version="7.2.3" /> - <PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" /> - <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> - <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" /> - <PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" /> + <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1"/> + <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/> + <PackageReference Include="Polly" Version="7.2.3"/> + <PackageReference Include="SixLabors.ImageSharp" Version="3.0.1"/> + <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/> + <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0"/> + <PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0"/> </ItemGroup> </Project> diff --git a/Constants.cs b/Constants.cs index ddd03fe5358fd0671e03aa774b8874bb72be6813..174458dcbd64d7f5cf0e95894b0da50c02f7092f 100644 --- a/Constants.cs +++ b/Constants.cs @@ -15,11 +15,9 @@ public abstract class Constants public const string Surname = "surname"; public const string Username = "username"; public const string UserSignUpDate = "userSignUpDate"; - public const string UserProfilePicture = "userProfilePicture"; public const string AccessToken = "accessToken"; public const string RefreshToken = "refreshToken"; - public const string ProfileSasToken = "profileSasToken"; } public static class AzureBlobContainers diff --git a/Controllers/AuthenticationController.cs b/Controllers/AuthenticationController.cs index a2855d39f422f1a684110cd5a6368f7d23274330..55a5fd72b517876ca000631ea3f6ad6e7dd26220 100644 --- a/Controllers/AuthenticationController.cs +++ b/Controllers/AuthenticationController.cs @@ -18,11 +18,11 @@ using Swashbuckle.AspNetCore.Annotations; [ApiController] public class AuthController : BaseAuthController { - private readonly IAppCache _memoryCache; private readonly IAuthService _authService; - private readonly IUserService _userService; private readonly IEmailService _emailService; + private readonly IAppCache _memoryCache; private readonly IRefreshTokenService _refreshTokenService; + private readonly IUserService _userService; public AuthController(IAppCache memoryCache, IAuthService authService, IRefreshTokenService refreshTokenService, IUserService userService, IEmailService emailService) diff --git a/Dockerfile b/Dockerfile index 1d122ac5e3dcc27ff64be76ae2d016f9dfd37491..27c13b0e5bee3ba3179c9c3e35122810e448f147 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:8.0-preview AS base +FROM mcr.microsoft.com/dotnet/sdk:8.0.100-preview.3 AS base WORKDIR /app EXPOSE 80 +EXPOSE 5001 +EXPOSE 8080 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:8.0-preview AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0.100-preview.3 AS build WORKDIR /src COPY ["AuthenticationMicroservice.csproj", "."] RUN dotnet restore "./AuthenticationMicroservice.csproj" diff --git a/Program.cs b/Program.cs index b99b65587160433dc59c51b8613f2c080f246319..01deefb6321e5e72d06b15419b3f67d7ada60374 100644 --- a/Program.cs +++ b/Program.cs @@ -17,16 +17,17 @@ using Swashbuckle.AspNetCore.SwaggerUI; var builder = WebApplication.CreateBuilder(args); // Add services to the container. - builder.Services.AddControllers(); builder.Services.AddOptions(); builder.Configuration.AddJsonFile("appsettings.Development.json", true, true); -builder.Services.Configure<EmailConfig>(builder.Configuration.GetSection("EmailConfig")); +builder.Configuration.AddEnvironmentVariables(); builder.Services.Configure<TokenSettings>(builder.Configuration.GetSection("TokenSettings")); +builder.Services.Configure<FrontendStrings>(builder.Configuration.GetSection("FrontendStrings")); +builder.Services.Configure<EmailConfig>(builder.Configuration.GetSection("EmailConfig")); +builder.Services.Configure<ConnectionStrings>(builder.Configuration.GetSection("ConnectionStrings")); builder.Services.AddDbContext<AuthenticationDbContext>(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("DbConnectionString") ?? throw new Exception("Could not connect to database."))); -builder.Services.Configure<ConnectionStrings>(builder.Configuration.GetSection("ConnectionStrings")); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo {Title = "AuthenticationMicroserviceAPI", Version = "v1"}); @@ -73,6 +74,17 @@ builder.Services.AddAuthentication(options => { options.DefaultScheme = "smart"; ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; + j.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + + if (!string.IsNullOrEmpty(accessToken)) context.Token = accessToken; + + return Task.CompletedTask; + } + }; }).AddPolicyScheme("smart", "Authorization", options => { options.ForwardDefaultSelector = _ => JwtBearerDefaults.AuthenticationScheme; }); @@ -81,22 +93,15 @@ builder.Services.AddAuthorization(options => options.AddPolicy(Constants.AuthorizationPolicies.HasUserId, policy => policy.RequireClaim(ClaimTypes.Sid)); }); -builder.Services.AddCors(options => -{ - options.AddDefaultPolicy(corsPolicyBuilder => corsPolicyBuilder - .WithOrigins("https://localhost:5295") // TODO: replace with Vue frontend url - .AllowAnyMethod() - .AllowAnyHeader()); -}); builder.Services.AddCors(options => { options.AddDefaultPolicy( corsPolicyBuilder => { - var origins = builder.Configuration.GetSection("Group17Website")["BaseUrl"]; corsPolicyBuilder.WithOrigins(builder.Configuration.GetSection("Group17Website")["BaseUrl"] ?? string.Empty) .AllowAnyHeader() - .AllowAnyMethod(); + .AllowAnyMethod() + .AllowCredentials(); }); }); diff --git a/Repositories/BaseRepository.cs b/Repositories/BaseRepository.cs index ace3136aed38c6aae6abd7c65ccd42e03039d855..fb7d57bfd21c5ed8fa335ab8120fb1adf97b20a1 100644 --- a/Repositories/BaseRepository.cs +++ b/Repositories/BaseRepository.cs @@ -26,8 +26,8 @@ public interface IBaseRepository<TEntity> where TEntity : class public sealed class BaseRepository<TEntity> : IBaseRepository<TEntity> where TEntity : class { - private readonly DbSet<TEntity> _dbSet; private readonly AuthenticationDbContext _context; + private readonly DbSet<TEntity> _dbSet; public BaseRepository(AuthenticationDbContext context) { diff --git a/Services/AuthService.cs b/Services/AuthService.cs index ec35e5e2025eb7909cd20142f5d932cc1fb2c4ab..f1a4ce99adefa42d26913278dfe4a6694d7b8f19 100644 --- a/Services/AuthService.cs +++ b/Services/AuthService.cs @@ -26,10 +26,10 @@ public interface IAuthService public class AuthService : IAuthService { - private readonly IEmailService _emailService; - private readonly IStorageService _storageService; private readonly AuthenticationDbContext _context; + private readonly IEmailService _emailService; private readonly IRefreshTokenService _refreshTokenService; + private readonly IStorageService _storageService; public AuthService(AuthenticationDbContext context, IEmailService emailService, IRefreshTokenService refreshTokenService, IStorageService storageService) @@ -68,11 +68,7 @@ public class AuthService : IAuthService public async Task<AuthenticatedUserDTO> GenerateAuthenticatedUser(User user, string? oldToken = null) { - var newAccess = _refreshTokenService.CreateNewTokenForUser(user, - !string.IsNullOrWhiteSpace(user.ProfilePictureUrl) - ? _storageService.GetSasForFile(Constants.AzureBlobContainers.ProfilePictures, user.ProfilePictureUrl) - ?.Query - : null); + var newAccess = _refreshTokenService.CreateNewTokenForUser(user); var newRefresh = string.IsNullOrWhiteSpace(oldToken) ? await _refreshTokenService.GenerateRefreshToken(user.Id) : await _refreshTokenService.RefreshToken(oldToken, user.Id); @@ -85,8 +81,7 @@ public class AuthService : IAuthService RefreshTokenExpires = newRefresh.Expires, ProfilePictureUrl = user.ProfilePictureUrl, ProfilePictureSas = _storageService.GetSasForFile(Constants.AzureBlobContainers.ProfilePictures, - user.ProfilePictureUrl ?? string.Empty) - ?.Query, + user.ProfilePictureUrl ?? string.Empty), UserId = user.Id }; diff --git a/Services/RefreshTokenService.cs b/Services/RefreshTokenService.cs index 92c32dc2cbe0a44ed0efc5cbfde7f63b8b0db6a0..2d3ece6ce0fd8bb80c06d9f7c8fa7ff854954224 100644 --- a/Services/RefreshTokenService.cs +++ b/Services/RefreshTokenService.cs @@ -17,24 +17,24 @@ using Settings; public interface IRefreshTokenService { - Task<AccessToken> CreateNewTokenForUser(int userId, string? sasToken = null); - AccessToken CreateNewTokenForUser(User user, string? sasToken = null); + Task<AccessToken> CreateNewTokenForUser(int userId); + AccessToken CreateNewTokenForUser(User user); Task<RefreshToken?> GenerateRefreshToken(int userId); Task<RefreshToken?> RefreshToken(string? oldToken, int userId); Task<RefreshToken?> GetRefreshTokenForUser(int userId); ClaimsPrincipal? GetPrincipalFromExpiredToken(string token); int GetUserIdFromExpiredToken(string token); Task InvalidateTokens(int userId, string reason); - Task<List<Claim>> GetClaimsForUser(int userId, string? sasToken = null); - List<Claim> GetClaimsForUser(User user, string? sasToken = null); + Task<List<Claim>> GetClaimsForUser(int userId); + List<Claim> GetClaimsForUser(User user); } public class RefreshTokenService : IRefreshTokenService { - private readonly TokenSettings _tokenSettings; private readonly AuthenticationDbContext _context; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IBaseRepository<RefreshToken> _refreshTokenRepo; + private readonly TokenSettings _tokenSettings; public RefreshTokenService(IOptions<TokenSettings> tokenSettings, AuthenticationDbContext context, IBaseRepository<RefreshToken> refreshTokenRepo, @@ -46,14 +46,14 @@ public class RefreshTokenService : IRefreshTokenService _httpContextAccessor = httpContextAccessor; } - public async Task<AccessToken> CreateNewTokenForUser(int userId, string? sasToken = null) + public async Task<AccessToken> CreateNewTokenForUser(int userId) { var user = await _context.User.FirstOrDefaultAsync(u => u.Id == userId); if (user == null) throw new AuthenticationException("User not found.", HttpStatusCode.Unauthorized); var tokenHandler = new JwtSecurityTokenHandler(); - var claims = GetClaimsForUser(user, sasToken); + var claims = GetClaimsForUser(user); var subject = new ClaimsIdentity(claims); @@ -72,10 +72,10 @@ public class RefreshTokenService : IRefreshTokenService }; } - public AccessToken CreateNewTokenForUser(User user, string? sasToken = null) + public AccessToken CreateNewTokenForUser(User user) { var tokenHandler = new JwtSecurityTokenHandler(); - var claims = GetClaimsForUser(user, sasToken); + var claims = GetClaimsForUser(user); var subject = new ClaimsIdentity(claims); @@ -94,7 +94,7 @@ public class RefreshTokenService : IRefreshTokenService }; } - public async Task<List<Claim>> GetClaimsForUser(int userId, string? sasToken = null) + public async Task<List<Claim>> GetClaimsForUser(int userId) { var user = await _context.User.FirstOrDefaultAsync(u => u.Id == userId); if (user == null) @@ -107,15 +107,13 @@ public class RefreshTokenService : IRefreshTokenService new(Constants.Claims.Firstname, user.FirstName ?? string.Empty), new(Constants.Claims.Surname, user.Surname ?? string.Empty), new(Constants.Claims.Username, user.Username), - new(Constants.Claims.UserProfilePicture, user.ProfilePictureUrl ?? string.Empty), - new(Constants.Claims.ProfileSasToken, sasToken ?? string.Empty), new(Constants.Claims.UserSignUpDate, user.CreatedAt.ToUnixTimeSeconds().ToString()) }; return claims; } - public List<Claim> GetClaimsForUser(User user, string? sasToken = null) + public List<Claim> GetClaimsForUser(User user) { var claims = new List<Claim> { @@ -124,8 +122,6 @@ public class RefreshTokenService : IRefreshTokenService new(Constants.Claims.Firstname, user.FirstName ?? string.Empty), new(Constants.Claims.Surname, user.Surname ?? string.Empty), new(Constants.Claims.Username, user.Username), - new(Constants.Claims.UserProfilePicture, user.ProfilePictureUrl ?? string.Empty), - new(Constants.Claims.ProfileSasToken, sasToken ?? string.Empty), new(Constants.Claims.UserSignUpDate, user.CreatedAt.ToUnixTimeSeconds().ToString()) }; diff --git a/Services/StorageService.cs b/Services/StorageService.cs index 8f1f4d2b12455135dd77f047da31bbe321557072..9e4bbf626e2dcdfdd04415f9f59e8a57cbfeee0e 100644 --- a/Services/StorageService.cs +++ b/Services/StorageService.cs @@ -12,7 +12,7 @@ public interface IStorageService Task<Uri> SaveImageAsJpgBlob(string containerName, string fileName, Stream input, Dictionary<string, string>? metadata = null); - Uri? GetSasForFile(string containerName, string url, DateTimeOffset? expires = null); + string? GetSasForFile(string containerName, string url, DateTimeOffset? expires = null); } public class StorageService : IStorageService @@ -24,7 +24,7 @@ public class StorageService : IStorageService _connectionStrings = connectionStrings.Value; } - public Uri? GetSasForFile(string containerName, string url, DateTimeOffset? expires = null) + public string? GetSasForFile(string containerName, string url, DateTimeOffset? expires = null) { url = GetFileFromUrl(url, containerName) ?? string.Empty; if (string.IsNullOrWhiteSpace(url)) @@ -32,7 +32,7 @@ public class StorageService : IStorageService var container = new BlobContainerClient(_connectionStrings.AzureBlobStorage, containerName); var blob = container.GetBlobClient(url); var urlWithSas = blob.GenerateSasUri(BlobSasPermissions.Read, DateTimeOffset.Now.AddHours(3)); - return urlWithSas; + return urlWithSas.Query; } public async Task<Uri> SaveImageAsJpgBlob(string containerName, string fileName, Stream input, diff --git a/Services/UserService.cs b/Services/UserService.cs index bd651a5b4e10b990ab7736c4cfbb9f9a39a64ef4..626784bc068e510e354a4b624f8371fc563b43e0 100644 --- a/Services/UserService.cs +++ b/Services/UserService.cs @@ -39,6 +39,7 @@ public class UserService : IUserService if (_userRepo.GetAll().AsNoTracking() .FirstOrDefault(u => string.Equals(u.EmailAddress, request.EmailAddress)) != null) throw new AuthenticationException("User with that email address already registered. Login instead."); + SecurityHelper.EnsurePasswordComplexity(request.Password, request.ConfirmPassword); var newUser = new User { FirstName = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(request.FirstName), @@ -62,8 +63,7 @@ public class UserService : IUserService ms); user.ProfilePictureUrl = image.ToString(); await _userRepo.UpdateAndSaveAsync(user); - return _storageService.GetSasForFile(Constants.AzureBlobContainers.ProfilePictures, image.ToString()) - ?.ToString(); + return _storageService.GetSasForFile(Constants.AzureBlobContainers.ProfilePictures, image.ToString()); } public List<UserDTO> GetListOfAllUsers() diff --git a/Settings/FrontendStrings.cs b/Settings/FrontendStrings.cs new file mode 100644 index 0000000000000000000000000000000000000000..71ce17a3f23a20292d51d3ac0558096047e4d728 --- /dev/null +++ b/Settings/FrontendStrings.cs @@ -0,0 +1,6 @@ +namespace AuthenticationMicroservice.Settings; + +public class FrontendStrings +{ + public string? BaseUrl { get; set; } +} \ No newline at end of file