alternative identity

This commit is contained in:
Michele 2020-11-07 15:25:06 +00:00
parent 585ac7c672
commit 85d8e4e6c6
34 changed files with 921 additions and 216 deletions

View file

@ -9,14 +9,12 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/IdentityServer/bin/Debug/netcoreapp3.1/IdentityServer.dll",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/netcoreapp3.1/WebApi.dll",
"args": [],
"cwd": "${workspaceFolder}/IdentityServer",
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"internalConsoleOptions": "openOnSessionStart",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},

View file

@ -7,32 +7,7 @@
"type": "process",
"args": [
"build",
"${workspaceFolder}/IdentityServer/IdentityServer.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/IdentityServer/IdentityServer.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"${workspaceFolder}/IdentityServer/IdentityServer.csproj",
"${workspaceFolder}/WebApi.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],

View file

@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using AutoMapper;
using System.IdentityModel.Tokens.Jwt;
using WebApi.Helpers;
using Microsoft.Extensions.Options;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using WebApi.Services;
using WebApi.Entities;
using WebApi.Models.Users;
namespace WebApi.Controllers
{
[Authorize]
[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
private IUserService _userService;
private IMapper _mapper;
private readonly AppSettings _appSettings;
public UsersController(
IUserService userService,
IMapper mapper,
IOptions<AppSettings> appSettings)
{
_userService = userService;
_mapper = mapper;
_appSettings = appSettings.Value;
}
[AllowAnonymous]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]AuthenticateModel model)
{
var user = _userService.Authenticate(model.Username, model.Password);
if (user == null)
return BadRequest(new { message = "Username or password is incorrect" });
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.Id.ToString())
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
// return basic user info and authentication token
return Ok(new
{
Id = user.Id,
Username = user.Username,
FirstName = user.FirstName,
LastName = user.LastName,
Token = tokenString
});
}
[AllowAnonymous]
[HttpPost("register")]
public IActionResult Register([FromBody]RegisterModel model)
{
// map model to entity
var user = _mapper.Map<User>(model);
try
{
// create user
_userService.Create(user, model.Password);
return Ok();
}
catch (AppException ex)
{
// return error message if there was an exception
return BadRequest(new { message = ex.Message });
}
}
[HttpGet]
public IActionResult GetAll()
{
var users = _userService.GetAll();
var model = _mapper.Map<IList<UserModel>>(users);
return Ok(model);
}
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
var user = _userService.GetById(id);
var model = _mapper.Map<UserModel>(user);
return Ok(model);
}
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody]UpdateModel model)
{
// map model to entity and set id
var user = _mapper.Map<User>(model);
user.Id = id;
try
{
// update user
_userService.Update(user, model.Password);
return Ok();
}
catch (AppException ex)
{
// return error message if there was an exception
return BadRequest(new { message = ex.Message });
}
}
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
_userService.Delete(id);
return Ok();
}
}
}

12
Identity/Entities/User.cs Normal file
View file

@ -0,0 +1,12 @@
namespace WebApi.Entities
{
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
}
}

View file

@ -0,0 +1,19 @@
using System;
using System.Globalization;
namespace WebApi.Helpers
{
// Custom exception class for throwing application specific exceptions (e.g. for validation)
// that can be caught and handled within the application
public class AppException : Exception
{
public AppException() : base() {}
public AppException(string message) : base(message) { }
public AppException(string message, params object[] args)
: base(String.Format(CultureInfo.CurrentCulture, message, args))
{
}
}
}

View file

@ -0,0 +1,7 @@
namespace WebApi.Helpers
{
public class AppSettings
{
public string Secret { get; set; }
}
}

View file

@ -0,0 +1,16 @@
using AutoMapper;
using WebApi.Entities;
using WebApi.Models.Users;
namespace WebApi.Helpers
{
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap<User, UserModel>();
CreateMap<RegisterModel, User>();
CreateMap<UpdateModel, User>();
}
}
}

View file

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using WebApi.Entities;
namespace WebApi.Helpers
{
public class DataContext : DbContext
{
protected readonly IConfiguration Configuration;
public DataContext(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// connect to sql server database
options.UseSqlServer(Configuration.GetConnectionString("WebApiDatabase"));
}
public DbSet<User> Users { get; set; }
}
}

View file

@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace WebApi.Helpers
{
public class SqliteDataContext : DataContext
{
public SqliteDataContext(IConfiguration configuration) : base(configuration) { }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// connect to sqlite database
options.UseSqlite(Configuration.GetConnectionString("WebApiDatabase"));
}
}
}

View file

@ -1,39 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using IdentityServer4.Models;
using System.Collections.Generic;
namespace IdentityServer
{
public static class Config
{
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("seldgemapperApi", "Sledgemapper API")
};
public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "seldgemapperApi" }
}
};
}
}

View file

@ -1,12 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
</ItemGroup>
</Project>

View file

@ -1,60 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
using System;
namespace IdentityServer
{
public class Program
{
public static int Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information)
.Enrich.FromLogContext()
// uncomment to write to Azure diagnostics stream
//.WriteTo.File(
// @"D:\home\LogFiles\Application\identityserver.txt",
// fileSizeLimitBytes: 1_000_000,
// rollOnFileSizeLimit: true,
// shared: true,
// flushToDiskInterval: TimeSpan.FromSeconds(1))
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Code)
.CreateLogger();
try
{
Log.Information("Starting host...");
CreateHostBuilder(args).Build().Run();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly.");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

View file

@ -1,12 +0,0 @@
{
"profiles": {
"SelfHost": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001"
}
}
}

View file

@ -1,60 +0,0 @@
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace IdentityServer
{
public class Startup
{
public IWebHostEnvironment Environment { get; }
public Startup(IWebHostEnvironment environment)
{
Environment = environment;
}
public void ConfigureServices(IServiceCollection services)
{
// uncomment, if you want to add an MVC-based UI
//services.AddControllersWithViews();
var builder = services.AddIdentityServer(options =>
{
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
})
// .AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients);
// not recommended for production - you need to store your key material somewhere secure
builder.AddDeveloperSigningCredential();
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// uncomment if you want to add MVC
//app.UseStaticFiles();
//app.UseRouting();
app.UseIdentityServer();
// uncomment, if you want to add MVC
//app.UseAuthorization();
//app.UseEndpoints(endpoints =>
//{
// endpoints.MapDefaultControllerRoute();
//});
}
}
}

View file

@ -1 +0,0 @@
{"alg":"RS256","d":"u88MEy28-4hRvzgL8MRiihCCqiNRJMkslHMr4NX64Xe_3MIxsfDsp-PF69y8OXYl_wlpWwshosmxBWWiRQNEFyJd6XnuLbOPPbEAvvApCDYEAiVqa9K40pCUPL2ICKk_Ix2qAjDzBAAK8Jvyrl_sth4JSw0IOHrZ77dtljLroII9EZaCcFEaUtFt58zKTg0fphqpZJUPjyAcabtHPCEfJKJFpPulnqOQA_BGhbT3Vpdo-N8gtLbVGzIbe5eOMxXf637STyfBrvkJN9BWQMm46H1XWoDaOD34ZbdbIC84xQn9igLeA1QCLP4C4NNNCkdcqpwd2nE7YoSS9PMOwqR-NQ","dp":"POSsdi7VV0EM843kV59KwLn8hYWTjcG6ACZMK5kKVDJ4tm6CDLJ-1MxCaSQwo2jJ6bRRATNLfmjfePU9qEuQ89oomZ-y0KeppKtwaGbsBPLv3LAjacwSzHY6jHvlEa04BZIHqjQc7MMBzARG68sAc3sbfYGwqGSSEtr01bfMAps","dq":"vGkZsdK_bg71EfDoHBAvknNPcXZ3gIjHxYiSZW8r5ZEawe862wcZcMAzI4PkkNcM9jJxWEWmKZvKORFVJstpdgrpNddblWbw4po_QySe2XBtyh1eplYybmZ7-HWdY-ogkJBWtzUdifGqhyjCtgkxV46pPWse3i_ymVV31Wt6Rak","e":"AQAB","kid":"28F80A128E9BFE8960B690D9A7AB7C83","kty":"RSA","n":"1gLbbP6yT36nnrkn6tBbJyulnhMA13uA8_588b4GBkCTFi4v9R193EJQCg28l6cm-_E93mnlc-_C4-ul1MSoUXehOZxayMzGlMANUVBCRUhazfY8lf3bf0vY1enGOYYQVl-_5w0PhoO5H1zIC7ohYkogivnOIH-QO9RrgwgBwpkm58rsT1pOscFrVKSWObp7_pHksBd3uXFnIDlIdvOGcptyD5YoYteS34Z3GRlgmvdm6Sq4oEwk_zgSWwiDLJtzFQsc4OGVoOBFRO0BxrF6CVrp6nEni6toYLyEHpwqoM56if07RSCihY2CqNFBM_FZa6V1YumC67pdWsjFjitxIQ","p":"3F_djAqfvrj-oyYDC_C1oKUTqDaGp-7OcwxKC86bCiWzeiS71gzbj1i5b4hCfjQgUsJNvpu15ETmc-buKstxjR0AJwVy5K5AjXAh-3BDzCHThVnIeG_NAK3PUtUsgpkfooBLClvnaiahDw55mqrqR2I4huFIkH5cE-s-OYuCq6M","q":"-Jul1jY6mO2F1DMESxaYIAMkvkh-dXn9420_olMs6Eu0-FyDGAdc1kzry0HJ94ZEOk0v8sI-jD8GZUncdQY_jHxGWUYqqQfDhJp4rYgkxUI89Odd40j3l2UDr1rY-ue3PHlY6xf17yGbpCXnS3UUhIaCPMKFmq62LXCi2N2WvGs","qi":"AmReUGiL7X8GwXiv_tjpqFwrGslqFkoIYz0Kq26MCTapDk0O8_qgdE5o5wdI2v87rvEGrGwY8vmmW0Okcfu52xxYxpp28-QPbTnjKCsQbsA7jOV7nejWqeQMTjdSzDd2lY0zvof_xLlHDr9ov2Qigj7ZUwqQ3xzdaAvziDQ0LdE"}

21
Identity/LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2019 Jason Watmore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,53 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using WebApi.Helpers;
namespace WebApi.Migrations.SqlServerMigrations
{
[DbContext(typeof(DataContext))]
[Migration("20200102103423_InitialCreate")]
partial class InitialCreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("WebApi.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("FirstName")
.HasColumnType("nvarchar(max)");
b.Property<string>("LastName")
.HasColumnType("nvarchar(max)");
b.Property<byte[]>("PasswordHash")
.HasColumnType("varbinary(max)");
b.Property<byte[]>("PasswordSalt")
.HasColumnType("varbinary(max)");
b.Property<string>("Username")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,34 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace WebApi.Migrations.SqlServerMigrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
FirstName = table.Column<string>(nullable: true),
LastName = table.Column<string>(nullable: true),
Username = table.Column<string>(nullable: true),
PasswordHash = table.Column<byte[]>(nullable: true),
PasswordSalt = table.Column<byte[]>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View file

@ -0,0 +1,51 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using WebApi.Helpers;
namespace WebApi.Migrations.SqlServerMigrations
{
[DbContext(typeof(DataContext))]
partial class DataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.0")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("WebApi.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("FirstName")
.HasColumnType("nvarchar(max)");
b.Property<string>("LastName")
.HasColumnType("nvarchar(max)");
b.Property<byte[]>("PasswordHash")
.HasColumnType("varbinary(max)");
b.Property<byte[]>("PasswordSalt")
.HasColumnType("varbinary(max)");
b.Property<string>("Username")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,49 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using WebApi.Helpers;
namespace WebApi.Migrations.SqliteMigrations
{
[DbContext(typeof(SqliteDataContext))]
[Migration("20200102102942_InitialCreate")]
partial class InitialCreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.0");
modelBuilder.Entity("WebApi.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("FirstName")
.HasColumnType("TEXT");
b.Property<string>("LastName")
.HasColumnType("TEXT");
b.Property<byte[]>("PasswordHash")
.HasColumnType("BLOB");
b.Property<byte[]>("PasswordSalt")
.HasColumnType("BLOB");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,34 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace WebApi.Migrations.SqliteMigrations
{
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
FirstName = table.Column<string>(nullable: true),
LastName = table.Column<string>(nullable: true),
Username = table.Column<string>(nullable: true),
PasswordHash = table.Column<byte[]>(nullable: true),
PasswordSalt = table.Column<byte[]>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Users");
}
}
}

View file

@ -0,0 +1,47 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using WebApi.Helpers;
namespace WebApi.Migrations.SqliteMigrations
{
[DbContext(typeof(SqliteDataContext))]
partial class SqliteDataContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.0");
modelBuilder.Entity("WebApi.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("FirstName")
.HasColumnType("TEXT");
b.Property<string>("LastName")
.HasColumnType("TEXT");
b.Property<byte[]>("PasswordHash")
.HasColumnType("BLOB");
b.Property<byte[]>("PasswordSalt")
.HasColumnType("BLOB");
b.Property<string>("Username")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace WebApi.Models.Users
{
public class AuthenticateModel
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
}

View file

@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace WebApi.Models.Users
{
public class RegisterModel
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace WebApi.Models.Users
{
public class UpdateModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace WebApi.Models.Users
{
public class UserModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Username { get; set; }
}
}

21
Identity/Program.cs Normal file
View file

@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
namespace WebApi
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>()
.UseUrls("http://localhost:4000");
});
}
}

View file

@ -0,0 +1,10 @@
{
"profiles": {
"Development": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

5
Identity/README.md Normal file
View file

@ -0,0 +1,5 @@
# aspnet-core-3-registration-login-api
ASP.NET Core 3.1 - Simple API for User Management, Authentication and Registration
For documentation and instructions check out https://jasonwatmore.com/post/2019/10/14/aspnet-core-3-simple-api-for-authentication-registration-and-user-management

View file

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Linq;
using WebApi.Entities;
using WebApi.Helpers;
namespace WebApi.Services
{
public interface IUserService
{
User Authenticate(string username, string password);
IEnumerable<User> GetAll();
User GetById(int id);
User Create(User user, string password);
void Update(User user, string password = null);
void Delete(int id);
}
public class UserService : IUserService
{
private DataContext _context;
public UserService(DataContext context)
{
_context = context;
}
public User Authenticate(string username, string password)
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return null;
var user = _context.Users.SingleOrDefault(x => x.Username == username);
// check if username exists
if (user == null)
return null;
// check if password is correct
if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
return null;
// authentication successful
return user;
}
public IEnumerable<User> GetAll()
{
return _context.Users;
}
public User GetById(int id)
{
return _context.Users.Find(id);
}
public User Create(User user, string password)
{
// validation
if (string.IsNullOrWhiteSpace(password))
throw new AppException("Password is required");
if (_context.Users.Any(x => x.Username == user.Username))
throw new AppException("Username \"" + user.Username + "\" is already taken");
byte[] passwordHash, passwordSalt;
CreatePasswordHash(password, out passwordHash, out passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
_context.Users.Add(user);
_context.SaveChanges();
return user;
}
public void Update(User userParam, string password = null)
{
var user = _context.Users.Find(userParam.Id);
if (user == null)
throw new AppException("User not found");
// update username if it has changed
if (!string.IsNullOrWhiteSpace(userParam.Username) && userParam.Username != user.Username)
{
// throw error if the new username is already taken
if (_context.Users.Any(x => x.Username == userParam.Username))
throw new AppException("Username " + userParam.Username + " is already taken");
user.Username = userParam.Username;
}
// update user properties if provided
if (!string.IsNullOrWhiteSpace(userParam.FirstName))
user.FirstName = userParam.FirstName;
if (!string.IsNullOrWhiteSpace(userParam.LastName))
user.LastName = userParam.LastName;
// update password if provided
if (!string.IsNullOrWhiteSpace(password))
{
byte[] passwordHash, passwordSalt;
CreatePasswordHash(password, out passwordHash, out passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
}
_context.Users.Update(user);
_context.SaveChanges();
}
public void Delete(int id)
{
var user = _context.Users.Find(id);
if (user != null)
{
_context.Users.Remove(user);
_context.SaveChanges();
}
}
// private helper methods
private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
using (var hmac = new System.Security.Cryptography.HMACSHA512())
{
passwordSalt = hmac.Key;
passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
}
}
private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt))
{
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i]) return false;
}
}
return true;
}
}
}

106
Identity/Startup.cs Normal file
View file

@ -0,0 +1,106 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using WebApi.Helpers;
using WebApi.Services;
using AutoMapper;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System;
namespace WebApi
{
public class Startup
{
private readonly IWebHostEnvironment _env;
private readonly IConfiguration _configuration;
public Startup(IWebHostEnvironment env, IConfiguration configuration)
{
_env = env;
_configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// use sql server db in production and sqlite db in development
if (_env.IsProduction())
services.AddDbContext<DataContext>();
else
services.AddDbContext<DataContext, SqliteDataContext>();
services.AddCors();
services.AddControllers();
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
// configure strongly typed settings objects
var appSettingsSection = _configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
// configure jwt authentication
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
var userId = int.Parse(context.Principal.Identity.Name);
var user = userService.GetById(userId);
if (user == null)
{
// return unauthorized if user no longer exists
context.Fail("Unauthorized");
}
return Task.CompletedTask;
}
};
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
// configure DI for application services
services.AddScoped<IUserService, UserService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext dataContext)
{
// migrate any database changes on startup (includes initial db creation)
dataContext.Database.Migrate();
app.UseRouting();
// global cors policy
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
}

18
Identity/WebApi.csproj Normal file
View file

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="9.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.6.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,12 @@
{
"ConnectionStrings": {
"WebApiDatabase": "Data Source=LocalDatabase.db"
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

16
Identity/appsettings.json Normal file
View file

@ -0,0 +1,16 @@
{
"AppSettings": {
"Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING"
},
"ConnectionStrings": {
"WebApiDatabase": "ENTER PRODUCTION SQL SERVER CONNECTION STRING HERE"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}