Been trying to get email sending to work in Blazor to allow users to confirm emails and reset passwords. I have followed the documentation https://learn.microsoft.com/en-us/aspnet/core/security/authentication/accconfirm?view=aspnetcore-3.1&tabs=visual-studio and looked over it over and over but I can't seem to figure out why the email does not send.
API Key has been set to Full Access
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options => {
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});
// Need to do this as it maps "role" to ClaimTypes.Role and causes issues
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddControllersWithViews();
services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration);
services.AddRazorPages();
services.Configure<IdentityOptions>(options =>
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier);
services.AddControllers().AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize);
}
public class AuthMessageSenderOptions
{
public string SendGridKey { get; set; }
}
Email sender
public class EmailSender : IEmailSender
{
public EmailSender(IOptions<AuthMessageSenderOptions> optionsAccessor)
{
Options = optionsAccessor.Value;
}
public AuthMessageSenderOptions Options { get; }
public Task SendEmailAsync(string email, string subject, string message)
{
return Execute(Options.SendGridKey, subject, message, email);
}
public Task Execute(string apiKey, string subject, string message, string email)
{
var client = new SendGridClient(apiKey);
var msg = new SendGridMessage()
{
From = new EmailAddress("info#example.com", "Identity Demo"),
Subject = subject,
PlainTextContent = message,
HtmlContent = message
};
msg.AddTo(new EmailAddress(email));
// Disable click tracking.
// See https://sendgrid.com/docs/User_Guide/Settings/tracking.html
msg.SetClickTracking(false, false);
return client.SendEmailAsync(msg);
}
}
RegisterConfirmation.cshtml:
DisplayConfirmAccountLink = false;
Related
This question already has answers here:
What is a NullReferenceException, and how do I fix it?
(27 answers)
Closed last year.
I'm trying to get all users with roles I have done it before and it's working in my other web apps and it's the same code.
I seeded Default Roles and default user with a role and registered other some users.
I get this error :NullReferenceException: Object reference not set to an instance of an object. at line var users = await _userManager.Users.ToListAsync();
I debugged it and the _userManager is null, I tried some solutions and gave the same Exception.
so what did I miss?
Program.CS
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.EntityFrameworkCore;
using EmoloyeeSystemApp.Areas.Identity.Data;
using EmployeeSystemApp.Data;
using EmoloyeeSystemApp.Areas;
//using EmoloyeeSystemApp.Migrations
using Microsoft.AspNetCore.Identity.UI.Services;
//using Employee_System.Models;
using Microsoft.Extensions.DependencyInjection;
using System;
using EmoloyeeSystemApp.Models;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<EmployeeSystemAppContext>(options =>
options.UseSqlServer(connectionString), ServiceLifetime.Scoped);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<EmployeeSystemAppUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
options.Password.RequireLowercase = false;
options.Password.RequireUppercase = false;
options.Password.RequireNonAlphanumeric = false;
})
.AddRoles<IdentityRole>()
.AddDefaultUI()
.AddEntityFrameworkStores<EmployeeSystemAppContext>()
.AddDefaultTokenProviders();
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddDbContext<EmployeeSystemAppContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
//builder.Services.AddAuthorization(options =>
//{
// //options.AddPolicy("rolecreation", policy => policy.RequireRole("Admin"));
//});
var app = builder.Build();
using (IServiceScope? scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
try
{
var context = services.GetRequiredService<EmployeeSystemAppContext>();
var userManager = services.GetRequiredService<UserManager<EmployeeSystemAppUser>>();
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
await Seeds.SeedRoles(userManager, roleManager);
await Seeds.SeedUser(userManager, roleManager);
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<Program>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.Run();
controller:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using EmoloyeeSystemApp.Areas.Identity.Data;
using EmployeeSystemApp.Models;
using EmoloyeeSystemApp.Areas;
namespace EmployeeSystemApp.Controllers
{
public class UsersWithRolesController : Controller
{
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<EmployeeSystemAppUser> _userManager;
public UsersWithRolesController(RoleManager<IdentityRole> roleManager, UserManager<EmployeeSystemAppUser> userManager)
{
roleManager = _roleManager;
userManager = _userManager;
}
private async Task<List<string>> GetUserRoles(EmployeeSystemAppUser user)
{
return new List<string>(await _userManager.GetRolesAsync(user));
}
//get all users with thier were assigned roles
public async Task <IActionResult> Index()
{
var users = await _userManager.Users.ToListAsync();
var usersRoles = new List<UsersWRoles>();
foreach(EmployeeSystemAppUser user in users)
{
var details = new UsersWRoles();
details.UserId = user.Id;
details.FirstName = user.FirstName;
details.LastName = user.LastName;
details.UserName = user.UserName;
details.Roles = await GetUserRoles(user);
}
return View(usersRoles);
}
public async Task<IActionResult> Manage(string userId)
{
//Get the user by Id
ViewBag.userId = userId;
var user = await _userManager.FindByIdAsync(userId);
//define of UserName
ViewBag.UserNanme = user.UserName;
// catch the possibility that there is no userId
if (user == null)
{
ViewBag.Erorr = $"User with Id = {userId} cannot be found";
return View("cannot be found");
}
var model = new List<ManageUsersAndRoles>();
foreach (var role in _roleManager.Roles)
{
//define constructor based on "ManageUsersAndRoles" Model
var usersRolesManage = new ManageUsersAndRoles()
{
RoleId = role.Id,
RoleName = role.Name
};
if (await _userManager.IsInRoleAsync(user, role.Name))
{
usersRolesManage.Selected = true;
}
else
{
usersRolesManage.Selected = false;
}
model.Add(usersRolesManage);
}
return View(model);
}
[HttpPost]
public async Task<IActionResult> Manage(List<ManageUsersAndRoles> model, string userId)
{
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return View();
}
var roles = await _userManager.GetRolesAsync(user);
var result = await _userManager.RemoveFromRolesAsync(user, roles);
if (!result.Succeeded)
{
ModelState.AddModelError("", "Cannot remove user's existing roles");
return View(model);
}
result = await _userManager.AddToRolesAsync(user, model.Where(x => x.Selected).Select(y => y.RoleName));
if (!result.Succeeded)
{
ModelState.AddModelError("", "Cannot add selected roles to user");
return View(model);
}
return RedirectToAction("Index");
}
}
}
Model:
namespace EmployeeSystemApp.Models
{
public class UsersWRoles
{
public string UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
public IEnumerable<string> Roles { get; set; }
}
}
it was a mistake in the controller in the constructor
_userManager = userManager;
it was backwards so I preferred to change the constructor to be :
private readonly UserManager<EmployeeSystemAppUser> userManager;
public UsersWithRolesController(RoleManager<IdentityRole> roleManager, UserManager<EmployeeSystemAppUser> userManager)
{
this.userManager = userManager;
}
and Add a missing line here to be :
foreach (EmployeeSystemAppUser user in users)
{
var details = new UsersWRoles();
details.UserId = user.Id;
details.FirstName = user.FirstName;
details.LastName = user.LastName;
details.UserName = user.UserName;
details.Roles = await GetUserRoles(user);
usersRoles.Add(details);
}
Can't get any detail about the user after he logged in although client side have the AspNetCore.Identity.Application token. Seem like he isn't authenticated although the cookies are set correctly. I searched a lot of question and didn't manage to solve it yet.
Startup.cs:
public class Startup {
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) {
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddDefaultIdentity<ApplicationUser>(options => {
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = true;
options.SignIn.RequireConfirmedAccount = false;
options.SignIn.RequireConfirmedEmail = false;
options.SignIn.RequireConfirmedEmail = false;
}
/*options => options.SignIn.RequireConfirmedAccount = true*/)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddControllersWithViews();
services.AddRazorPages();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration => {
configuration.RootPath = "ClientApp/dist";
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.ConfigureApplicationCookie(options => {
// Cookie settings
options.Cookie.HttpOnly = false;
//options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
//options.LoginPath = "/Account/Login";
//options.AccessDeniedPath = "/Identity/Account/AccessDenied";
//options.SlidingExpiration = true;
});
//services.AddIdentity<ApplicationUser, IdentityRole>(config => {
// config.SignIn.RequireConfirmedEmail = false;
//});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseMigrationsEndPoint();
}
else {
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment()) {
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.UseSpa(spa => {
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment()) {
spa.UseAngularCliServer(npmScript: "start");
}
});
}
}
IdentityHostingStartup.cs:
public class IdentityHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices((context, services) => {
});
}
}
Relavant controller:
private SignInManager<ApplicationUser> _signManager; //User
private UserManager<ApplicationUser> _userManager;
public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signManager) {
_userManager = userManager;
_signManager = signManager;
}
// GET: AccountController
public ActionResult Index() {
return View();
}
// GET: AccountController/Details/5
public ActionResult Details(int id) {
return View();
}
// GET: AccountController/Create
public ActionResult Create() {
return View();
}
// POST: AccountController/Create
[HttpPost]
public async Task<JsonResult> Create([FromBody] RegistrtionData registrtionData) {
if (registrtionData != null) {
var user = new ApplicationUser { UserName = registrtionData.name, Email = registrtionData.email };
var result = await _userManager.CreateAsync(user, registrtionData.password);
return Json(result);
}
return Json("null data");
}
[HttpPost]
//[ValidateAntiForgeryToken]
public async Task<ActionResult> Login([FromBody] LoginData loginData) {
if (loginData != null) {
var result = await _signManager.PasswordSignInAsync(loginData.userName,
loginData.password, true, false);
return Ok("login result" + result);
}
return Ok("got null data as login data");
}
[HttpPost]
public async Task<JsonResult> Logout() {
try {
await _signManager.SignOutAsync();
return Json(true);
} catch (Exception e) {
return Json(false);
}
}
ApplicationUser.cs:
public class ApplicationUser : IdentityUser {
//public string password { get; set; }
//public string name { get; set; }
//public string familyName { get; set; }
//public int age { get; set; }
}
Client Side:
nSubmit(formData: NgForm) {
this.serverService.login(formData.form.value).subscribe(result => {
if (result.succeeded) {
this.loginMeassage = ''
this.router.navigate(['/heroScreen'])
}
else {
this.loginMeassage = 'ERROR, password or username incorrect!!'
}
}, error => console.error('error', error));
Network request, response cookie (jwt) ontop, and the req cookie down (it's suppose to change every communication??):
User after more then 1 request:
OK, so after wasting probably 15 hours on this..
Turns out u need to set DefaultAuthenticateScheme and DefaultChallengeScheme.
So instead of:
services.AddAuthentication().AddIdentityServerJwt();
Do this:
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
}).AddIdentityServerJwt();
Also, possible unrelated walkaround (but kinda ugly):
var cookie = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
var user = await _userManager.GetUserAsync(cookie.Principal);
hope it's will help somebody someday.
i desided to login with steam in asp.net core 2.1 ,
i use AspNet.Security.OpenId.Steam nuget package for connection
,when call sigin method ,client page redirect to steam and after login with steam call back to my server,but not authenticaed request and rejected...
1-in Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env,IConfiguration configuration,ApplicationDbContext applicationDbContext,ApplicationDbContextBase applicationDbContextBase)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseHsts();
}
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseCors(option => option.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
app.UseStaticFiles();
app.UseAuthentication();
app.UseHttpsRedirection();
AppHttpContext.Configure(app.ApplicationServices.GetRequiredService<IHttpContextAccessor>());
applicationDbContext.MigrateToLastChange();
}
2 - in service.cs
public static IServiceCollection SetupNegatechApi(this IServiceCollection services, IConfiguration configuration)
{
//TODO: add services here...
services.AddMvc()
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver();
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
//Assign User & Role Model And DbContext To Identity
services.AddIdentity<ApplicationIdentityUser, ApplicationIdentityRole>().AddDefaultTokenProviders().AddEntityFrameworkStores<ApplicationDbContextBase>();
//Get Auth Key & Convert To Byte;
var AuthInfo = configuration.GetSection("Auth").Get<AppSettings>();
var SSKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AuthInfo.SecurityKey));
//Config Identity Password & JWT Config
services.Configure<IdentityOptions>(options =>
{
options.Password.RequiredLength = 6;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Password.RequireDigit = false;
})
.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(option =>
{
option.RequireHttpsMetadata = false;
option.SaveToken = true;
option.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = AuthInfo.Issuer,
ValidAudience = AuthInfo.Audienc,
IssuerSigningKey = SSKey,
ClockSkew = TimeSpan.Zero
};
})
.AddCookie()
.AddSteam(op =>
{
configuration.Bind(op);
op.ClaimsIssuer = AuthInfo.Issuer;
op.SaveTokens = true;
op.CallbackPath = "/api/Steam/SteamCallBack";
op.RequireHttpsMetadata = false;
});
services.Configure<IISOptions>(op => op.AutomaticAuthentication = false);
//Register Configuration For Dependncy Injection
services.AddSingleton<IConfiguration>(configuration);
services.AddSingleton<IFileProvider>(new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/$gallery")));
return services;
}
3-in Controller
[ApiController]
[ApiExplorerSettings(GroupName = "public")]
[Route("api/[controller]/[action]")]
public class SteamController : BaseController
{
[HttpPost]
public async Task<IActionResult> Signin()
{
var auth = new AuthenticationProperties { RedirectUri = "/api/Steam/SteamCallBack" };
return Challenge(auth,"Steam" );
}
[HttpGet]
public IActionResult SteamCallBack(string state,openid openid)
{
//breack point
return Redirect("http://localhost:3000/profile?id=" + "test");
}
}
public class openid
{
public string claimed_id { get; set; }
public string identity { get; set; }
public string return_to { get; set; }
public string response_nonce { get; set; }
public string assoc_handle { get; set; }
public string signed { get; set; }
public string sig { get; set; }
}
4-in html file
<form id="steam_form" action="https://localhost:44315/api/Steam/Signin" method="post">
//Submit Login form to api server
<button type="submit"> Login</button>
</form>
5- result error after call back http://s8.picofile.com/file/8365103326/Untitled.png
I don't know why, but AddSteam option is above the OpenID rules.
If you look closer, then you can see that Steams OpenId is only name and some random standards.
Check your form with change endpoint to your.address/signin and make a post form:
<form id="steamAuth" action="https://localhost:44315/signin" method="post">
<input type='hidden' name='Provider' value='Steam'>
<input type = 'hidden' name='ReturnUrl' value='your.address/returnurl'></form>
<button type="submit"> Login</button>
</form>
Im not sure, but i think that .AddSteam() option not including any settings added in services configuration.
If you check repo of this library you can see examples, and here its just AddSteam(), when other providers are described:
services.AddAuthentication(options => { /* Authentication options */ })
.AddSteam()
.AddOpenId("StackExchange", "StackExchange", options =>
{
options.Authority = new Uri("https://openid.stackexchange.com/");
options.CallbackPath = "/signin-stackexchange";
});
I just setup a basic integration to Identity Framework Core 2.0.1 in my app. I've configured IdentityUser to use INT instead of STRING as its primary key as per this article (https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-primary-key-configuration?tabs=aspnetcore2x).
I'm using code first with a blank database created via an initial migration.
The problem I'm running into is that when I create a user (I'm using postman), I get back the following exception:
Cannot insert explicit value for identity column in table 'AspNetUsers' when IDENTITY_INSERT is set to OFF.
I checked the value of appUser.Id when it is passed to UserManager.CreateAsync and it is definitely = 0 (which is the CLR default value for an Int property)
If I configure AppUser to use a String as a primary key (the default settings), the Id column is not set as an identity column and I get no error - the user is created without a problem with a GUID as the ID generated internally by the Identity framework.
From postman:
{
"email":"someemail#comcast.net",
"password":"Password123",
"firstName":"Alex",
"lastName":"Florin",
"userName":"aflorin"
}
AppUser.cs
using Microsoft.AspNetCore.Identity;
namespace Driver.Models.Entities
{
public class AppUser : IdentityUser<int>
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
AppRole.cs
using Microsoft.AspNetCore.Identity;
namespace Driver.Models.Entities
{
public class AppRole : IdentityRole<int> {
}
}
IAppUserRepository.cs
using System.Threading.Tasks;
namespace Driver.Repositories
{
public interface IAppUserRepository<AppUser>
{
Task Create(AppUser appUser, string password);
}
}
AppUserRepository.cs
using Driver.DBContext;
using Driver.Models.Entities;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;
namespace Driver.Repositories
{
public class AppUserRepository : IAppUserRepository<AppUser>
{
private UserManager<AppUser> _userManager;
protected DriverDbContext _dbContext;
public AppUserRepository(UserManager<AppUser> userManager, DriverDbContext dbContext)
{
_userManager = userManager;
_dbContext = dbContext;
}
public async Task Create(AppUser appUser, string password)
{
var result = await _userManager.CreateAsync(appUser, password);
//ToDo: if (!result.Succeeded) { }
await _dbContext.SaveChangesAsync();
}
}
}
AccountsController.cs
using Driver.Models.Entities;
using Driver.ViewModels.Identity;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Driver.Repositories;
namespace Driver.Controllers
{
[Produces("application/json")]
[Route("api/[controller]")]
public class AccountsController : Controller
{
private IAppUserRepository<AppUser> _appUserRepository;
public AccountsController(UserManager<AppUser> userManager, IAppUserRepository<AppUser> appUserRepository)
{
_appUserRepository = appUserRepository;
}
[HttpPost]
public async Task<IActionResult> Create([FromBody]RegistrationViewModel registrationVM)
{
if (registrationVM == null)
{
return BadRequest();
}
var appUser = Mapper.Map<AppUser>(registrationVM);
await _appUserRepository.Create(appUser, registrationVM.Password);
return CreatedAtAction("Create", new { id = appUser.Id }, Mapper.Map<RegistrationViewModel>(appUser));
}
}
}
RegistrationViewModel.cs
namespace Driver.ViewModels.Identity
{
public class RegistrationViewModel
{
public string Email { get; set; }
public string Password { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string UserName { get; set; }
}
}
ConfigureServices from Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DriverDbContext>(options => options.UseSqlServer(_config.GetConnectionString("DriverDBConnection")));
services.AddSingleton<IJwtFactory, JwtFactory>();
services.TryAddTransient<IHttpContextAccessor, HttpContextAccessor>();
services.AddIdentity<AppUser, IdentityRole<int>>()
.AddEntityFrameworkStores<DriverDbContext>()
.AddDefaultTokenProviders();
var settings = _config.GetSection("Authentication").Get<AuthenticationAppSettings>();
// Configure JwtIssuerOptions
services.Configure((Models.JwtIssuerOptions options) =>
{
options.Issuer = settings.JwtIssuerOptions.Issuer;
options.Audience = settings.JwtIssuerOptions.Audience;
options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});
// Specify the validation parameters to dictate how we want received tokens validated
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = settings.JwtIssuerOptions.Issuer,
ValidateAudience = true,
ValidAudience = settings.JwtIssuerOptions.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = _signingKey,
RequireExpirationTime = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(configureOptions =>
{
configureOptions.ClaimsIssuer = settings.JwtIssuerOptions.Issuer;
configureOptions.TokenValidationParameters = tokenValidationParameters;
configureOptions.SaveToken = true;
});
// Create an authorization claim policy to guard API controllers and actions
services.AddAuthorization(options =>
{
options.AddPolicy("ApiUser", policy => policy.RequireClaim(Constants.Strings.JwtClaimIdentifiers.Rol, Constants.Strings.JwtClaims.ApiAccess));
});
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = settings.Password.RequiredDigit;
options.Password.RequiredLength = settings.Password.RequiredLength;
options.Password.RequireNonAlphanumeric = settings.Password.RequireNonAlphanumeric;
options.Password.RequireUppercase = settings.Password.RequireUppercase;
options.Password.RequireLowercase = settings.Password.RequireLowercase;
options.Password.RequiredUniqueChars = settings.Password.RequiredUniqueChars;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(settings.Lockout.DefaultLockoutTimeSpan);
options.Lockout.MaxFailedAccessAttempts = settings.Lockout.MaxFailedAccessAttempts;
options.Lockout.AllowedForNewUsers = settings.Lockout.AllowedForNewUsers;
// User settings
options.User.RequireUniqueEmail = settings.User.RequireUniqueEmail;
});
services.AddMvc().AddJsonOptions(
options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
services.AddAutoMapper(typeof(Startup));
services.AddScoped<ICompanyRepository, CompanyRepository>();
services.AddScoped<ILookupRepository, LookupRepository>();
services.AddScoped<IManagerRepository, ManagerRepository>();
services.AddScoped<IAppUserRepository<AppUser>, AppUserRepository>();
}
The class declaration line for my DbContext (I'm not doing any custom configuration on the AppUser entity):
public class DriverDbContext : IdentityDbContext<AppUser, AppRole, int>
I have a couple of legacy ASP.NET web apps that share a database for ASP.NET Membership. I want to move to a microservices architecture utilizing .NET Core and IdentityServer4 and have the identity server in the new microservices ecosystem to use the existing ASP.NET Membership user store, but .NET Core doesn't appear to support ASP.NET Membership at all.
I currently have a proof of concept stood up involving a web API, identity server and an MVC web app as my client. The identity server implements a subclass of IdentityUser and implements IUserStore/IUserPasswordStore/IUserEmailStore to adapt it to the ASP.NET Membership tables in my existing database. I can register new users and login via my POC MVC client app but these users cannot log into my legacy apps. Conversely, users registered in legacy apps can't log into my POC MVC client. I assume its because my implementation of IPasswordHasher isn't hashing the passwords the same as ASP.NET Membership in my legacy apps.
Below is my code. Any insight into what I might be doing wrong would be greatly appreciated. Security and cryptography are not my strong suit.
Startup.cs
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsDevelopment())
{
// For more details on using the user secret store see https://go.microsoft.com/fwlink/?LinkID=532709
builder.AddUserSecrets<Startup>();
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
/* Add CORS policy */
services.AddCors(options =>
{
// this defines a CORS policy called "default"
options.AddPolicy("default", policy =>
{
policy.WithOrigins("http://localhost:5003")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
/* Add MVC componenets. */
services.AddMvc();
/* Configure IdentityServer. */
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
// Cookie settings
options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(150);
options.Cookies.ApplicationCookie.LoginPath = "/Account/Login";
options.Cookies.ApplicationCookie.LogoutPath = "/Account/Logout";
// User settings
options.User.RequireUniqueEmail = true;
});
/* Add the DbContext */
services.AddDbContext<StoreContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MyConnectionString")));
/* Add ASP.NET Identity to use for registration and authentication. */
services.AddIdentity<AspNetMembershipUser, IdentityRole>()
.AddEntityFrameworkStores<StoreContext>()
.AddUserStore<AspNetMembershipUserStore>()
.AddDefaultTokenProviders();
services.AddTransient<IPasswordHasher<AspNetMembershipUser>, AspNetMembershipPasswordHasher>();
/* Add IdentityServer and its components. */
services.AddIdentityServer()
.AddInMemoryCaching()
.AddTemporarySigningCredential()
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
/* Configure logging. */
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
if (env.IsDevelopment())
{
loggerFactory.AddDebug();
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
/* Configure wwwroot */
app.UseStaticFiles();
/* Configure CORS */
app.UseCors("default");
/* Configure AspNet Identity */
app.UseIdentity();
/* Configure IdentityServer */
app.UseIdentityServer();
/* Configure MVC */
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
AspNetMembershipUser.cs
public class AspNetMembershipUser : IdentityUser
{
public string PasswordSalt { get; set; }
public int PasswordFormat { get; set; }
}
AspNetMembershipUserStore.cs
public class AspNetMembershipUserStore : IUserStore<AspNetMembershipUser>, IUserPasswordStore<AspNetMembershipUser>, IUserEmailStore<AspNetMembershipUser>
{
private readonly StoreContext _dbcontext;
public AspNetMembershipUserStore(StoreContext dbContext)
{
_dbcontext = dbContext;
}
public Task<IdentityResult> CreateAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
try
{
User dbUser = new User();
this.Convert(user, dbUser);
_dbcontext.Users.Add(dbUser);
_dbcontext.SaveChanges();
return IdentityResult.Success;
}
catch (Exception ex)
{
return IdentityResult.Failed(new IdentityError
{
Code = ex.GetType().Name,
Description = ex.Message
});
}
});
}
public Task<IdentityResult> DeleteAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
try
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.ProviderUserName == user.NormalizedUserName);
if (dbUser != null)
{
_dbcontext.AspNetUsers.Remove(dbUser.AspNetUser);
_dbcontext.Users.Remove(dbUser);
_dbcontext.SaveChanges();
}
return IdentityResult.Success;
}
catch (Exception ex)
{
return IdentityResult.Failed(new IdentityError
{
Code = ex.GetType().Name,
Description = ex.Message
});
}
});
}
public void Dispose()
{
_dbcontext.Dispose();
}
public Task<AspNetMembershipUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.ProviderEmailAddress == normalizedEmail);
if (dbUser == null)
{
return null;
}
AspNetMembershipUser user = new AspNetMembershipUser();
this.Convert(dbUser, user);
return user;
});
}
public Task<AspNetMembershipUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
{
long lUserId = long.Parse(userId);
return Task.Factory.StartNew(() =>
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u=> u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.UserId == lUserId);
if (dbUser == null)
{
return null;
}
AspNetMembershipUser user = new AspNetMembershipUser();
this.Convert(dbUser, user);
return user;
});
}
public Task<AspNetMembershipUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.ProviderUserName == normalizedUserName);
if (dbUser == null)
{
return null;
}
AspNetMembershipUser user = new AspNetMembershipUser();
this.Convert(dbUser, user);
return user;
});
}
public Task<string> GetEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.Email);
}
public Task<bool> GetEmailConfirmedAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.EmailConfirmed);
}
public Task<string> GetNormalizedEmailAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedEmail);
}
public Task<string> GetNormalizedUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedUserName);
}
public Task<string> GetPasswordHashAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.PasswordHash);
}
public Task<string> GetUserIdAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.Id.ToString());
}
public Task<string> GetUserNameAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.UserName);
}
public Task<bool> HasPasswordAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => !string.IsNullOrEmpty(user.PasswordHash));
}
public Task SetEmailAsync(AspNetMembershipUser user, string email, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.Email = email);
}
public Task SetEmailConfirmedAsync(AspNetMembershipUser user, bool confirmed, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.EmailConfirmed = confirmed);
}
public Task SetNormalizedEmailAsync(AspNetMembershipUser user, string normalizedEmail, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedEmail = normalizedEmail);
}
public Task SetNormalizedUserNameAsync(AspNetMembershipUser user, string normalizedName, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.NormalizedUserName = normalizedName);
}
public Task SetPasswordHashAsync(AspNetMembershipUser user, string passwordHash, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.PasswordHash = passwordHash);
}
public Task SetUserNameAsync(AspNetMembershipUser user, string userName, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() => user.UserName = userName);
}
public Task<IdentityResult> UpdateAsync(AspNetMembershipUser user, CancellationToken cancellationToken)
{
return Task.Factory.StartNew(() =>
{
try
{
User dbUser = _dbcontext.Users
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetMembership)
.Include(u => u.AspNetUsers).ThenInclude(u => u.AspNetApplication)
.Include(u => u.UserGroups)
.SingleOrDefault(u => u.UserId.ToString() == user.Id);
if (dbUser != null)
{
this.Convert(user, dbUser);
_dbcontext.Users.Update(dbUser);
_dbcontext.SaveChanges();
}
return IdentityResult.Success;
}
catch(Exception ex)
{
return IdentityResult.Failed(new IdentityError
{
Code = ex.GetType().Name,
Description = ex.Message
});
}
});
}
private void Convert(User from, AspNetMembershipUser to)
{
to.Id = from.ProviderUserKey.ToString();
to.UserName = from.ProviderUserName;
to.NormalizedUserName = from.ProviderUserName.ToLower();
to.Email = from.ProviderEmailAddress;
to.NormalizedEmail = from.ProviderEmailAddress.ToLower();
to.EmailConfirmed = true;
to.PasswordHash = from.AspNetUser.AspNetMembership.Password;
to.PasswordSalt = from.AspNetUser.AspNetMembership.PasswordSalt;
to.PasswordFormat = from.AspNetUser.AspNetMembership.PasswordFormat;
to.AccessFailedCount = from.AspNetUser.AspNetMembership.FailedPasswordAttemptCount;
to.EmailConfirmed = true;
to.Roles.Clear();
from.UserGroups.ToList().ForEach(ug =>
{
to.Roles.Add(new IdentityUserRole<string>
{
RoleId = ug.GroupId.ToString(),
UserId = ug.UserId.ToString()
});
});
to.PhoneNumber = from.Phone ?? from.ShippingPhone;
to.PhoneNumberConfirmed = !string.IsNullOrEmpty(to.PhoneNumber);
to.SecurityStamp = from.AspNetUser.AspNetMembership.PasswordSalt;
}
private void Convert(AspNetMembershipUser from , User to)
{
AspNetApplication application = _dbcontext.AspNetApplications.First();
to.ProviderUserKey = Guid.Parse(from.Id);
to.ProviderUserName = from.UserName;
to.ProviderEmailAddress = from.Email;
to.InternalEmail = $"c_{Guid.NewGuid().ToString()}#mycompany.com";
to.AccountOwner = "MYCOMPANY";
to.UserStatusId = (int)UserStatus.Normal;
AspNetUser aspNetUser = to.AspNetUser;
if (to.AspNetUser == null)
{
to.AspNetUser = new AspNetUser
{
ApplicationId = application.ApplicationId,
AspNetApplication= application,
AspNetMembership = new AspNetMembership
{
ApplicationId = application.ApplicationId,
AspNetApplication = application
}
};
}
to.AspNetUser.UserId = Guid.Parse(from.Id);
to.AspNetUser.UserName = from.UserName;
to.AspNetUser.LoweredUserName = from.UserName.ToLower();
to.AspNetUser.LastActivityDate = DateTime.UtcNow;
to.AspNetUser.IsAnonymous = false;
to.AspNetUser.ApplicationId = application.ApplicationId;
to.AspNetUser.AspNetMembership.CreateDate = DateTime.UtcNow;
to.AspNetUser.AspNetMembership.Email = from.Email;
to.AspNetUser.AspNetMembership.IsApproved = true;
to.AspNetUser.AspNetMembership.LastLoginDate = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.LastLockoutDate = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.LastPasswordChangedDate = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.LoweredEmail = from.NormalizedEmail.ToLower();
to.AspNetUser.AspNetMembership.Password = from.PasswordHash;
to.AspNetUser.AspNetMembership.PasswordSalt = from.PasswordSalt;
to.AspNetUser.AspNetMembership.PasswordFormat = from.PasswordFormat;
to.AspNetUser.AspNetMembership.IsLockedOut = false;
to.AspNetUser.AspNetMembership.FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1754-01-01 00:00:00.000");
to.AspNetUser.AspNetMembership.FailedPasswordAttemptWindowStart = DateTime.Parse("1754-01-01 00:00:00.000");
// Merge Groups/Roles
to.UserGroups
.Where(ug => !from.Roles.Any(r => ug.GroupId.ToString() == r.RoleId))
.ToList()
.ForEach(ug => to.UserGroups.Remove(ug));
to.UserGroups
.Join(from.Roles, ug => ug.GroupId.ToString(), r => r.RoleId, (ug, r) => new { To = ug, From = r })
.ToList()
.ForEach(j =>
{
j.To.UserId = long.Parse(j.From.UserId);
j.To.GroupId = int.Parse(j.From.RoleId);
});
from.Roles
.Where(r => !to.UserGroups.Any(ug => ug.GroupId.ToString() == r.RoleId))
.ToList()
.ForEach(r =>
{
to.UserGroups.Add(new UserGroup
{
UserId = long.Parse(from.Id),
GroupId = int.Parse(r.RoleId)
});
});
}
}
AspNetMembershipPasswordHasher.cs
public class AspNetMembershipPasswordHasher : IPasswordHasher<AspNetMembershipUser>
{
private readonly int _saltSize;
private readonly int _bytesRequired;
private readonly int _iterations;
public AspNetMembershipPasswordHasher()
{
this._saltSize = 128 / 8;
this._bytesRequired = 32;
this._iterations = 1000;
}
public string HashPassword(AspNetMembershipUser user, string password)
{
string passwordHash = null;
string passwordSalt = null;
this.HashPassword(password, out passwordHash, ref passwordSalt);
user.PasswordSalt = passwordSalt;
return passwordHash;
}
public PasswordVerificationResult VerifyHashedPassword(AspNetMembershipUser user, string hashedPassword, string providedPassword)
{
// Throw an error if any of our passwords are null
if (hashedPassword == null)
{
throw new ArgumentNullException("hashedPassword");
}
if (providedPassword == null)
{
throw new ArgumentNullException("providedPassword");
}
string providedPasswordHash = null;
if (user.PasswordFormat == 0)
{
providedPasswordHash = providedPassword;
}
else if (user.PasswordFormat == 1)
{
string providedPasswordSalt = user.PasswordSalt;
this.HashPassword(providedPassword, out providedPasswordHash, ref providedPasswordSalt);
}
else
{
throw new NotSupportedException("Encrypted passwords are not supported.");
}
if (providedPasswordHash == hashedPassword)
{
return PasswordVerificationResult.Success;
}
else
{
return PasswordVerificationResult.Failed;
}
}
private void HashPassword(string password, out string passwordHash, ref string passwordSalt)
{
byte[] hashBytes = null;
byte[] saltBytes = null;
byte[] totalBytes = new byte[this._saltSize + this._bytesRequired];
if (!string.IsNullOrEmpty(passwordSalt))
{
// Using existing salt.
using (var pbkdf2 = new Rfc2898DeriveBytes(password, Convert.FromBase64String(passwordSalt), this._iterations))
{
saltBytes = pbkdf2.Salt;
hashBytes = pbkdf2.GetBytes(this._bytesRequired);
}
}
else
{
// Generate a new salt.
using (var pbkdf2 = new Rfc2898DeriveBytes(password, this._saltSize, this._iterations))
{
saltBytes = pbkdf2.Salt;
hashBytes = pbkdf2.GetBytes(this._bytesRequired);
}
}
Buffer.BlockCopy(saltBytes, 0, totalBytes, 0, this._saltSize);
Buffer.BlockCopy(hashBytes, 0, totalBytes, this._saltSize, this._bytesRequired);
using (SHA256 hashAlgorithm = SHA256.Create())
{
passwordHash = Convert.ToBase64String(hashAlgorithm.ComputeHash(totalBytes));
passwordSalt = Convert.ToBase64String(saltBytes);
}
}
}
One of my coworkers was able to help me out. Below is what the hash function should look like. With this change, ASP.NET Identity is able to piggy back on an existing ASP.NET Membership database.
private void HashPassword(string password, out string passwordHash, ref string passwordSalt)
{
byte[] passwordBytes = Encoding.Unicode.GetBytes(password);
byte[] saltBytes = null;
if (!string.IsNullOrEmpty(passwordSalt))
{
saltBytes = Convert.FromBase64String(passwordSalt);
}
else
{
saltBytes = new byte[128 / 8];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(saltBytes);
}
}
byte[] totalBytes = new byte[saltBytes.Length + passwordBytes.Length];
Buffer.BlockCopy(saltBytes, 0, totalBytes, 0, saltBytes.Length);
Buffer.BlockCopy(passwordBytes, 0, totalBytes, saltBytes.Length, passwordBytes.Length);
using (SHA1 hashAlgorithm = SHA1.Create())
{
passwordHash = Convert.ToBase64String(hashAlgorithm.ComputeHash(totalBytes));
}
passwordSalt = Convert.ToBase64String(saltBytes);
}
You can find all the source code on GitHib.