Skip to content

Commit

Permalink
fix: authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
BirajMainali committed Jan 28, 2024
1 parent 15eddf8 commit 09ba6ba
Show file tree
Hide file tree
Showing 17 changed files with 259 additions and 71 deletions.
6 changes: 6 additions & 0 deletions App.Base/Constants/RequestClientConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace App.Base.Constants;

public class RequestClientConstants
{
public const string RequestClientHeaderKey = "Request-Client";
}
6 changes: 6 additions & 0 deletions App.Base/Settings/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ public class AppSettings
public ConnectionStrings ConnectionStrings { get; set; }
public bool UseMultiTenancy { get; set; } = false;
public string DefaultDataProtectionPurpose { get; set; }
public JwtSettings JwtSettings { get; set; }
}

public class JwtSettings
{
public string Secret { get; set; }
}

public class ConnectionStrings
Expand Down
26 changes: 14 additions & 12 deletions App.User/Handler/MultiTenantHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using App.User.Entity;
using App.User.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

Expand Down Expand Up @@ -39,41 +38,44 @@ public async Task<AppUser> HandleAsync(UserDto dto, string tenantName)
{
tenantName = tenantName.IgnoreCase();
_logger.LogInformation("Request to create user {@dto} for tenant {tenantName}", dto, tenantName);
var user = await ResolveUser(dto, tenantName);
await ConfigureDatabaseIfRequired(dto, tenantName);
var (user, tenant) = await ResolveUser(dto, tenantName);
await ConfigureDatabaseIfRequired(tenant: tenant, user);
_logger.LogInformation("User created {@user}", user);
return user;
}

private async Task ConfigureDatabaseIfRequired(UserDto dto, string tenantName)
private async Task ConfigureDatabaseIfRequired(ApplicationTenant? tenant, AppUser user)
{
if (_options.Value.UseMultiTenancy)
{
_logger.LogInformation("Request to create user {@dto} for tenant {tenantName}", dto, tenantName);
var connectionString = _databaseConnectionProvider.GetConnectionString(tenantName.ToSnakeCase());
_logger.LogInformation("Request to create user {@dto} for tenant {tenantName}", user, tenant!.Name);
var connectionString = _databaseConnectionProvider.GetConnectionString(tenant.Name.ToSnakeCase());
_context.Database.SetConnectionString(connectionString);
_logger.LogInformation("Connection string set to {connectionString}", connectionString);
await _context.Database.MigrateAsync();
_logger.LogInformation("Database migrated");
_logger.LogInformation("Connection string set to {connectionString}", tenantName.ToSnakeCase());
await ResolveUser(dto, tenantName);
_logger.LogInformation("Connection string set to {connectionString}", tenant.Name.ToSnakeCase());
await _uow.CreateAsync(user);
await _uow.CreateAsync(tenant);
user.SetTenant(tenant);
await _uow.CommitAsync();
}
}

private async Task<AppUser> ResolveUser(UserDto dto, string tenantName)
private async Task<(AppUser user, ApplicationTenant? applicationTenant)> ResolveUser(UserDto dto, string tenantName)
{
using var tsc = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
var user = await _userService.CreateUser(dto);
ApplicationTenant applicationTenant = null;
if (_options.Value.UseMultiTenancy)
{
var applicationTenant = new ApplicationTenant(tenantName, tenantName.ToSnakeCase());
applicationTenant = new ApplicationTenant(tenantName, tenantName.ToSnakeCase());
await _uow.CreateAsync(applicationTenant);
user.SetTenant(applicationTenant);
return user;
}

await _uow.CommitAsync();
tsc.Complete();
return user;
return (user, applicationTenant);
}
}
4 changes: 2 additions & 2 deletions App.User/Manager/IMultiTenantClaimManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace App.User.Manager;

public interface IMultiTenantClaimManager : IScopedDependency
{
string SaveClaims(Dictionary<string, string> claims);
string GetClaimsFromRequest();
string GetProtectedClaim(string key);
string GetMultiTenantConnectionKey();
void RemoveClaims();
}
34 changes: 7 additions & 27 deletions App.User/Manager/MultiTenantClaimManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class MultiTenantClaimManager : IMultiTenantClaimManager
private readonly IDataProtectionProvider _dataProtector;
private readonly IOptions<AppSettings> _options;

private readonly string _extraClaimsKey = ClaimsConstants.ProtectedClaimsKey;
private readonly string _extraClaimsKey = AuthenticationKeyConstants.MultiTenantAuthenticationKey;

public MultiTenantClaimManager(IHttpContextAccessor httpContextAccessor, IDataProtectionProvider dataProtector, IOptions<AppSettings> options)
{
Expand All @@ -23,39 +23,19 @@ public MultiTenantClaimManager(IHttpContextAccessor httpContextAccessor, IDataPr
_options = options;
}

public string SaveClaims(Dictionary<string, string> claims)
public string GetProtectedClaim(string key)
{
var claimsSerialized = claims.ToJson();
var protector = _dataProtector.CreateProtector(ResolvePurpose(_extraClaimsKey));
var protectedClaims = protector.Protect(claimsSerialized);
_httpContextAccessor.HttpContext?.Response.Cookies.Append(_extraClaimsKey, protectedClaims, new()
{
Expires = DateTimeOffset.Now.AddDays(2),
HttpOnly = true,
IsEssential = true
});
var protectedClaims = protector.Protect(key);
return protectedClaims;
}

public string GetClaimsFromRequest()
public string GetMultiTenantConnectionKey()
{
string value;
if (!_httpContextAccessor.HttpContext.Request.Cookies.TryGetValue(_extraClaimsKey, out value))
{
StringValues header;
if (_httpContextAccessor.HttpContext.Request.Headers.TryGetValue(_extraClaimsKey, out header))
{
value = header.FirstOrDefault();
}
}

if (value.ValueOrNull() == null)
{
return null;
}

var value = _httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(x => x.Type == _extraClaimsKey);
if (value == null) return null;
var protector = _dataProtector.CreateProtector(ResolvePurpose(_extraClaimsKey));
return protector.Unprotect(value);
return protector.Unprotect(value.Value);
}

public void RemoveClaims()
Expand Down
2 changes: 2 additions & 0 deletions App.Web/App.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageReference Include="AspNetCoreHero.ToastNotification" Version="1.1.0" />
<PackageReference Include="Dapper" Version="2.1.28" />
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.1" />
Expand All @@ -22,6 +23,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="7.2.0" />
<PackageReference Include="Npgsql" Version="8.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
Expand Down
50 changes: 48 additions & 2 deletions App.Web/Areas/Api/AuthenticationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
using App.User.Dto;
using App.User.Entity;
using App.User.Handler;
using App.Web.Manager.Interfaces;
using App.Web.Providers.Interfaces;
using App.Web.ViewModel;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Serilog;

Expand All @@ -12,15 +16,24 @@ namespace App.Web.Areas.Api;
[ApiController]
[Area("Api")]
[Route("[area]/[controller]")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class AuthenticationController : ControllerBase
{
private readonly IMultiTenantHandler _multiTenantHandler;
private readonly IRepository<AppUser, long> _userRepo;
private readonly ICurrentUserProvider _currentUserProvider;
private readonly IAuthenticator _authenticator;

public AuthenticationController(IMultiTenantHandler multiTenantHandler, IRepository<AppUser, long> userRepo)
public AuthenticationController(
IMultiTenantHandler multiTenantHandler,
IRepository<AppUser, long> userRepo,
ICurrentUserProvider currentUserProvider,
IAuthenticator authenticator)
{
_multiTenantHandler = multiTenantHandler;
_userRepo = userRepo;
_currentUserProvider = currentUserProvider;
_authenticator = authenticator;
}

[HttpGet]
Expand All @@ -37,7 +50,7 @@ public async Task<IActionResult> GetUsers()
}
}


[AllowAnonymous]
[HttpPost]
public async Task<IActionResult> Create([FromForm] UserVm vm)
{
Expand All @@ -54,4 +67,37 @@ public async Task<IActionResult> Create([FromForm] UserVm vm)
return this.SendError(e.Message);
}
}

[HttpPost]
[AllowAnonymous]
[Route("Login")]
public async Task<IActionResult> Login([FromForm] LoginVm vm)
{
try
{
var result = await _authenticator.AuthenticateThoughToken(vm.Email, vm.Password);
return this.SendSuccess("Success", result);
}
catch (Exception e)
{
Log.Error(e, "Error while logging in");
return this.SendError(e.Message);
}
}

[HttpGet]
[Route("WhatIsMyTenant")]
public IActionResult WhatIsMyTenant()
{
try
{
var connectionKey = _currentUserProvider.GetCurrentConnectionKey();
return this.SendSuccess("Success", connectionKey);
}
catch (Exception e)
{
Log.Error(e, "Error while getting tenant");
return this.SendError(e.Message);
}
}
}
92 changes: 88 additions & 4 deletions App.Web/DiConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using App.Base.Repository;
using System.Text;
using App.Base.Constants;
using App.Base.Repository;
using App.Base.Settings;
using App.Web.Data;
using App.Web.Manager;
Expand All @@ -7,7 +9,10 @@
using App.Web.Providers.Interfaces;
using AspNetCoreHero.ToastNotification;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;

namespace App.Web;
Expand All @@ -17,12 +22,83 @@ public static class ApplicationDiConfig
public static void UseApp(this WebApplicationBuilder builder)
{
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" }); });
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var secret = jwtSettings.GetSection("Secret").Value;


builder.Services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = "smart";
opt.DefaultChallengeScheme = "smart";
})
.AddPolicyScheme("smart", "JWT or Identity Cookie", options =>
{
options.ForwardDefaultSelector = context =>
{
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
if (authHeader?.ToLower().StartsWith("bearer ") == true)
{
return JwtBearerDefaults.AuthenticationScheme;
}

return CookieAuthenticationDefaults.AuthenticationScheme;
};
})
.AddCookie(cfg =>
{
cfg.SlidingExpiration = true;
cfg.LoginPath = "/Auth/Index";
cfg.ExpireTimeSpan = TimeSpan.FromDays(2);
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(1)
};
});

builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" });

c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer {token}\"",
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
});

builder.Services.AddNotyf(config =>
{
Expand All @@ -31,7 +107,7 @@ public static void UseApp(this WebApplicationBuilder builder)
config.Position = NotyfPosition.BottomRight;
});

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(x => { x.LoginPath = "/Auth"; });
builder.Services.Configure<AppSettings>(builder.Configuration);


builder.Services.AddControllersWithViews().AddRazorRuntimeCompilation();
Expand All @@ -40,8 +116,16 @@ public static void UseApp(this WebApplicationBuilder builder)
.AddScoped<DbContext, ApplicationDbContext>()
.AddScoped<IAuthenticator, Authenticator>().AddHttpContextAccessor();

builder.Services.Configure<AppSettings>(builder.Configuration);
builder.Services.ConfigureServices();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", corsPolicyBuilder =>
{
corsPolicyBuilder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});

builder.Services.AddScoped(typeof(IRepository<,>), typeof(Repository<,>));
}
Expand Down
Loading

0 comments on commit 09ba6ba

Please sign in to comment.