Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Identity/Auth to template #300 #487

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ indent_size = 2

# Code files
[*.{cs,csx,vb,vbx}]
indent_size =2
indent_size = 2
insert_final_newline = true
charset = utf-8-bom
###############################
Expand Down Expand Up @@ -61,18 +61,20 @@ dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Use PascalCase for constant fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
dotnet_naming_symbols.constant_fields.required_modifiers = const
tab_width=2
tab_width= 2
dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
dotnet_naming_rule.private_members_with_underscore.severity = suggestion
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.prefix_underscore.capitalization = camel_case
dotnet_naming_style.prefix_underscore.required_prefix = _
dotnet_style_operator_placement_when_wrapping = beginning_of_line
end_of_line = crlf
###############################
# C# Coding Conventions #
###############################
Expand Down Expand Up @@ -134,6 +136,12 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
###############################
# VB Coding Conventions #
###############################
Expand Down
4 changes: 4 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
<PackageVersion Include="FastEndpoints.Swagger.Swashbuckle" Version="2.0.1" />
<PackageVersion Include="FluentAssertions" Version="6.10.0" />
<PackageVersion Include="MediatR" Version="12.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="7.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="7.0.6" />
<PackageVersion Include="Moq" Version="4.18.4" />
Expand All @@ -34,6 +37,7 @@
<PackageVersion Include="SQLite" Version="3.13.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="6.25.1" />
<PackageVersion Include="xunit" Version="2.4.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" />
<PackageReference Include="Autofac" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" PrivateAssets="all" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" />
<PackageReference Include="SQLite" />
</ItemGroup>

Expand Down
2 changes: 1 addition & 1 deletion src/Clean.Architecture.Infrastructure/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public AppDbContext(DbContextOptions<AppDbContext> options,

public DbSet<ToDoItem> ToDoItems => Set<ToDoItem>();
public DbSet<Project> Projects => Set<Project>();
public DbSet<Contributor> Contributors => Set<Contributor>();
public DbSet<Contributor> Contributors => Set<Contributor>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Clean.Architecture.Core.Interfaces;
using Clean.Architecture.Core.ProjectAggregate;
using Clean.Architecture.Infrastructure.Data;
using Clean.Architecture.Infrastructure.Identity.Jwt;
using Clean.Architecture.SharedKernel;
using Clean.Architecture.SharedKernel.Interfaces;
using MediatR;
Expand Down Expand Up @@ -54,6 +55,9 @@ protected override void Load(ContainerBuilder builder)

private void RegisterCommonDependencies(ContainerBuilder builder)
{
builder.RegisterType<JwtService>()
.As<IJwtService>().InstancePerLifetimeScope();

builder.RegisterGeneric(typeof(EfRepository<>))
.As(typeof(IRepository<>))
.As(typeof(IReadRepository<>))
Expand All @@ -69,10 +73,10 @@ private void RegisterCommonDependencies(ContainerBuilder builder)
.As<IDomainEventDispatcher>()
.InstancePerLifetimeScope();


//builder.Register<ServiceFactory>(context =>
//{
// var c = context.Resolve<IComponentContext>();

// return t => c.Resolve(t);
//});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Reflection.Emit;
using System.Reflection;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Clean.Architecture.Infrastructure.Identity;
public class AppIdentityDbContext : IdentityDbContext<User, Role, int>
{
public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
// Customize the ASP.NET Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);
}
}
16 changes: 16 additions & 0 deletions src/Clean.Architecture.Infrastructure/Identity/Jwt/AccessToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.IdentityModel.Tokens.Jwt;

namespace Clean.Architecture.Infrastructure.Identity.Jwt;
public class AccessToken
{
public string Access_Token { get; set; }
public string TokenType { get; set; }
public int ExpiresIn { get; set; }

public AccessToken(JwtSecurityToken securityToken)
{
Access_Token = new JwtSecurityTokenHandler().WriteToken(securityToken);
TokenType = "Bearer";
ExpiresIn = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds;
}
}
12 changes: 12 additions & 0 deletions src/Clean.Architecture.Infrastructure/Identity/Jwt/IJwtService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Clean.Architecture.Infrastructure.Identity.Jwt;
public interface IJwtService
{
Task<AccessToken> GenerateAsync(User user);
int? ValidateJwtAccessTokenAsync(string token);
}
98 changes: 98 additions & 0 deletions src/Clean.Architecture.Infrastructure/Identity/Jwt/JwtService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace Clean.Architecture.Infrastructure.Identity.Jwt;
public class JwtService : IJwtService
{
private readonly SiteSettings _siteSetting;
private readonly UserManager<User> _userManager;

public JwtService(IOptionsSnapshot<SiteSettings> settings,
UserManager<User> userManager)
{
_siteSetting = settings.Value;
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
}

public async Task<AccessToken> GenerateAsync(User user)
{
var secretKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.SecretKey); // longer that 16 character
var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature);

//We can use EncryptingCredentials options in SecurityTokenDescriptor and hence our JWT token will not be parsed by jwt.io site and it will only be decrypted only by our code.
//Hence you can secure your token and who can see it
//var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character
//var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionKey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256);

var claims = await GetClaimsAsync(user);

var descriptor = new SecurityTokenDescriptor
{
Issuer = _siteSetting.JwtSettings.Issuer,
Audience = _siteSetting.JwtSettings.Audience,
IssuedAt = DateTime.Now,
NotBefore = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.NotBeforeMinutes),
Expires = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.ExpirationMinutes),
SigningCredentials = signingCredentials,
//EncryptingCredentials = encryptingCredentials,
Subject = new ClaimsIdentity(claims)
};

var tokenHandler = new JwtSecurityTokenHandler();

var securityToken = tokenHandler.CreateJwtSecurityToken(descriptor);

return new AccessToken(securityToken: securityToken);
}

public int? ValidateJwtAccessTokenAsync(string token)
{
var secretKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.SecretKey); // longer that 16 character

//if you are giving a value to EncryptingCredentials while generating a token then uncomment the encryptionKey and TokenDecryptionKey option so token can be parsed while validating.
//var encryptionKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character

var tokenHandler = new JwtSecurityTokenHandler();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(secretKey),
//TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey),
ValidateIssuer = false,
ValidateAudience = false,
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
ClockSkew = TimeSpan.Zero
}, out var validatedToken);

var jwtSecurityToken = (JwtSecurityToken)validatedToken;
var userId = int.Parse(jwtSecurityToken.Claims.First(claim => claim.Type == "nameid").Value);
return userId;
}
catch
{
throw;
}
}

private async Task<IEnumerable<Claim>> GetClaimsAsync(User user)
{
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
claims.Add(new Claim(ClaimTypes.Name, user.UserName!));

var userRoles = await _userManager.GetRolesAsync(user);

foreach (var role in userRoles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}

return claims;
}
}
8 changes: 8 additions & 0 deletions src/Clean.Architecture.Infrastructure/Identity/Role.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Clean.Architecture.SharedKernel.Interfaces;
using Microsoft.AspNetCore.Identity;

namespace Clean.Architecture.Infrastructure.Identity;
public class Role : IdentityRole<int>, IAggregateRoot
{
public string Description { get; set; } = default!;
}
33 changes: 33 additions & 0 deletions src/Clean.Architecture.Infrastructure/Identity/SiteSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Clean.Architecture.Infrastructure.Identity;
public class SiteSettings
{
public JwtSettings JwtSettings { get; set; } = default!;
public IdentitySettings IdentitySettings { get; set; } = default!;
}

public class IdentitySettings
{
public bool PasswordRequireDigit { get; set; }
public int PasswordRequiredLength { get; set; }
public bool PasswordRequireNonAlphanumeric { get; set; }
public bool PasswordRequireUppercase { get; set; }
public bool PasswordRequireLowercase { get; set; }
public bool RequireUniqueEmail { get; set; }
}

public class JwtSettings
{
public string SecretKey { get; set; } = default!;
public string EncryptKey { get; set; } = default!;
public string Issuer { get; set; } = default!;
public string Audience { get; set; } = default!;
public int NotBeforeMinutes { get; set; }
public int ExpirationMinutes { get; set; }
public int RefreshTokenValidityInDays { get; set; }
}
27 changes: 27 additions & 0 deletions src/Clean.Architecture.Infrastructure/Identity/User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Clean.Architecture.SharedKernel.Interfaces;
using Microsoft.AspNetCore.Identity;

namespace Clean.Architecture.Infrastructure.Identity;
public class User : IdentityUser<int>, IAggregateRoot
{
public User()
{
IsActive = true;
}

public string FullName { get; set; } = default!;

public int Age { get; set; }

public GenderType Gender { get; set; }

public bool IsActive { get; set; }

public DateTimeOffset? LastLoginDate { get; set; }
}

public enum GenderType
{
Male = 1,
Female = 2
}
Loading