├── .gitignore ├── AuthenticationServer.API ├── AuthenticationServer.API.csproj ├── AuthenticationServer.API.csproj.user ├── Controllers │ └── AuthenticationController.cs ├── Migrations │ ├── 20201119182451_initial.Designer.cs │ ├── 20201119182451_initial.cs │ ├── 20201211165943_Identity.Designer.cs │ ├── 20201211165943_Identity.cs │ └── AuthenticationDbContextModelSnapshot.cs ├── Models │ ├── AccessToken.cs │ ├── AuthenticationConfiguration.cs │ ├── AuthenticationDbContext.cs │ ├── RefreshToken.cs │ └── User.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── Authenticators │ │ └── Authenticator.cs │ ├── RefreshTokenRepositories │ │ ├── DatabaseRefreshTokenRepository.cs │ │ ├── IRefreshTokenRepository.cs │ │ └── InMemoryRefreshTokenRepository.cs │ ├── TokenGenerators │ │ ├── AccessTokenGenerator.cs │ │ ├── RefreshTokenGenerator.cs │ │ └── TokenGenerator.cs │ └── TokenValidators │ │ └── RefreshTokenValidator.cs ├── Startup.cs ├── appsettings.Development.json └── appsettings.json ├── AuthenticationServer.sln ├── ClientApplication ├── ClientApplication.csproj ├── Http │ └── AutoRefreshHttpMessageHandler.cs ├── Program.cs ├── Services │ ├── IDataService.cs │ ├── ILoginService.cs │ ├── IRefreshService.cs │ └── IRegisterService.cs └── Stores │ └── TokenStore.cs ├── Core ├── CoreLib.csproj ├── Requests │ ├── LoginRequest.cs │ ├── RefreshRequest.cs │ └── RegisterRequest.cs └── Responses │ ├── AuthenticatedUserResponse.cs │ ├── DataResponse.cs │ └── ErrorResponse.cs └── DataServer.API ├── Controllers └── HomeController.cs ├── DataServer.API.csproj ├── DataServer.API.csproj.user ├── Program.cs ├── Properties └── launchSettings.json ├── Startup.cs ├── appsettings.Development.json └── appsettings.json /.gitignore: -------------------------------------------------------------------------------- 1 | obj 2 | bin 3 | .vs 4 | *.db -------------------------------------------------------------------------------- /AuthenticationServer.API/AuthenticationServer.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /AuthenticationServer.API/AuthenticationServer.API.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ProjectDebugger 5 | 6 | 7 | AuthenticationServer.API 8 | 9 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Controllers/AuthenticationController.cs: -------------------------------------------------------------------------------- 1 | using AuthenticationServer.API.Models; 2 | using AuthenticationServer.API.Services.Authenticators; 3 | using AuthenticationServer.API.Services.RefreshTokenRepositories; 4 | using AuthenticationServer.API.Services.TokenValidators; 5 | using CoreLib.Requests; 6 | using CoreLib.Responses; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.AspNetCore.Identity; 9 | using Microsoft.AspNetCore.Mvc; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Linq; 13 | using System.Security.Claims; 14 | using System.Threading.Tasks; 15 | 16 | namespace AuthenticationServer.API.Controllers 17 | { 18 | public class AuthenticationController : Controller 19 | { 20 | private readonly UserManager _userRepository; 21 | private readonly Authenticator _authenticator; 22 | private readonly RefreshTokenValidator _refreshTokenValidator; 23 | private readonly IRefreshTokenRepository _refreshTokenRepository; 24 | 25 | public AuthenticationController(UserManager userRepository, 26 | Authenticator authenticator, 27 | RefreshTokenValidator refreshTokenValidator, 28 | IRefreshTokenRepository refreshTokenRepository) 29 | { 30 | _userRepository = userRepository; 31 | _authenticator = authenticator; 32 | _refreshTokenValidator = refreshTokenValidator; 33 | _refreshTokenRepository = refreshTokenRepository; 34 | } 35 | 36 | [HttpPost("register")] 37 | public async Task Register([FromBody] RegisterRequest registerRequest) 38 | { 39 | if(!ModelState.IsValid) 40 | { 41 | return BadRequestModelState(); 42 | } 43 | 44 | if (registerRequest.Password != registerRequest.ConfirmPassword) 45 | { 46 | return BadRequest(new ErrorResponse("Password does not match confirm password.")); 47 | } 48 | 49 | User registrationUser = new User() 50 | { 51 | Email = registerRequest.Email, 52 | UserName = registerRequest.Username 53 | }; 54 | 55 | IdentityResult result = await _userRepository.CreateAsync(registrationUser, registerRequest.Password); 56 | if(!result.Succeeded) 57 | { 58 | IdentityErrorDescriber errorDescriber = new IdentityErrorDescriber(); 59 | IdentityError primaryError = result.Errors.FirstOrDefault(); 60 | 61 | if(primaryError.Code == nameof(errorDescriber.DuplicateEmail)) 62 | { 63 | return Conflict(new ErrorResponse("Email already exists.")); 64 | } 65 | else if (primaryError.Code == nameof(errorDescriber.DuplicateUserName)) 66 | { 67 | return Conflict(new ErrorResponse("Username already exists.")); 68 | } 69 | } 70 | 71 | return Ok(); 72 | } 73 | 74 | [HttpPost("login")] 75 | public async Task Login([FromBody] LoginRequest loginRequest) 76 | { 77 | if(!ModelState.IsValid) 78 | { 79 | return BadRequestModelState(); 80 | } 81 | 82 | User user = await _userRepository.FindByNameAsync(loginRequest.Username); 83 | if(user == null) 84 | { 85 | return Unauthorized(); 86 | } 87 | 88 | bool isCorrectPassword = await _userRepository.CheckPasswordAsync(user, loginRequest.Password); 89 | if(!isCorrectPassword) 90 | { 91 | return Unauthorized(); 92 | } 93 | 94 | AuthenticatedUserResponse response = await _authenticator.Authenticate(user); 95 | 96 | return Ok(response); 97 | } 98 | 99 | [HttpPost("refresh")] 100 | public async Task Refresh([FromBody] RefreshRequest refreshRequest) 101 | { 102 | if (!ModelState.IsValid) 103 | { 104 | return BadRequestModelState(); 105 | } 106 | 107 | bool isValidRefreshToken = _refreshTokenValidator.Validate(refreshRequest.RefreshToken); 108 | if(!isValidRefreshToken) 109 | { 110 | return BadRequest(new ErrorResponse("Invalid refresh token.")); 111 | } 112 | 113 | RefreshToken refreshTokenDTO = await _refreshTokenRepository.GetByToken(refreshRequest.RefreshToken); 114 | if(refreshTokenDTO == null) 115 | { 116 | return NotFound(new ErrorResponse("Invalid refresh token.")); 117 | } 118 | 119 | await _refreshTokenRepository.Delete(refreshTokenDTO.Id); 120 | 121 | User user = await _userRepository.FindByIdAsync(refreshTokenDTO.UserId.ToString()); 122 | if(user == null) 123 | { 124 | return NotFound(new ErrorResponse("User not found.")); 125 | } 126 | 127 | AuthenticatedUserResponse response = await _authenticator.Authenticate(user); 128 | 129 | return Ok(response); 130 | } 131 | 132 | [Authorize] 133 | [HttpDelete("logout")] 134 | public async Task Logout() 135 | { 136 | string rawUserId = HttpContext.User.FindFirstValue("id"); 137 | 138 | if(!Guid.TryParse(rawUserId, out Guid userId)) 139 | { 140 | return Unauthorized(); 141 | } 142 | 143 | await _refreshTokenRepository.DeleteAll(userId); 144 | 145 | return NoContent(); 146 | } 147 | 148 | private IActionResult BadRequestModelState() 149 | { 150 | IEnumerable errorMessages = ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)); 151 | 152 | return BadRequest(new ErrorResponse(errorMessages)); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Migrations/20201119182451_initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using AuthenticationServer.API.Models; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace AuthenticationServer.API.Migrations 10 | { 11 | [DbContext(typeof(AuthenticationDbContext))] 12 | [Migration("20201119182451_initial")] 13 | partial class initial 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "5.0.0"); 20 | 21 | modelBuilder.Entity("AuthenticationServer.API.Models.RefreshToken", b => 22 | { 23 | b.Property("Id") 24 | .ValueGeneratedOnAdd() 25 | .HasColumnType("TEXT"); 26 | 27 | b.Property("Token") 28 | .HasColumnType("TEXT"); 29 | 30 | b.Property("UserId") 31 | .HasColumnType("TEXT"); 32 | 33 | b.HasKey("Id"); 34 | 35 | b.ToTable("RefreshTokens"); 36 | }); 37 | 38 | modelBuilder.Entity("AuthenticationServer.API.Models.User", b => 39 | { 40 | b.Property("Id") 41 | .ValueGeneratedOnAdd() 42 | .HasColumnType("TEXT"); 43 | 44 | b.Property("Email") 45 | .HasColumnType("TEXT"); 46 | 47 | b.Property("PasswordHash") 48 | .HasColumnType("TEXT"); 49 | 50 | b.Property("Username") 51 | .HasColumnType("TEXT"); 52 | 53 | b.HasKey("Id"); 54 | 55 | b.ToTable("Users"); 56 | }); 57 | #pragma warning restore 612, 618 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Migrations/20201119182451_initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace AuthenticationServer.API.Migrations 5 | { 6 | public partial class initial : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "RefreshTokens", 12 | columns: table => new 13 | { 14 | Id = table.Column(type: "TEXT", nullable: false), 15 | Token = table.Column(type: "TEXT", nullable: true), 16 | UserId = table.Column(type: "TEXT", nullable: false) 17 | }, 18 | constraints: table => 19 | { 20 | table.PrimaryKey("PK_RefreshTokens", x => x.Id); 21 | }); 22 | 23 | migrationBuilder.CreateTable( 24 | name: "Users", 25 | columns: table => new 26 | { 27 | Id = table.Column(type: "TEXT", nullable: false), 28 | Email = table.Column(type: "TEXT", nullable: true), 29 | Username = table.Column(type: "TEXT", nullable: true), 30 | PasswordHash = table.Column(type: "TEXT", nullable: true) 31 | }, 32 | constraints: table => 33 | { 34 | table.PrimaryKey("PK_Users", x => x.Id); 35 | }); 36 | } 37 | 38 | protected override void Down(MigrationBuilder migrationBuilder) 39 | { 40 | migrationBuilder.DropTable( 41 | name: "RefreshTokens"); 42 | 43 | migrationBuilder.DropTable( 44 | name: "Users"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Migrations/20201211165943_Identity.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using AuthenticationServer.API.Models; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace AuthenticationServer.API.Migrations 10 | { 11 | [DbContext(typeof(AuthenticationDbContext))] 12 | [Migration("20201211165943_Identity")] 13 | partial class Identity 14 | { 15 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 16 | { 17 | #pragma warning disable 612, 618 18 | modelBuilder 19 | .HasAnnotation("ProductVersion", "5.0.0"); 20 | 21 | modelBuilder.Entity("AuthenticationServer.API.Models.RefreshToken", b => 22 | { 23 | b.Property("Id") 24 | .ValueGeneratedOnAdd() 25 | .HasColumnType("TEXT"); 26 | 27 | b.Property("Token") 28 | .HasColumnType("TEXT"); 29 | 30 | b.Property("UserId") 31 | .HasColumnType("TEXT"); 32 | 33 | b.HasKey("Id"); 34 | 35 | b.ToTable("RefreshTokens"); 36 | }); 37 | 38 | modelBuilder.Entity("AuthenticationServer.API.Models.User", b => 39 | { 40 | b.Property("Id") 41 | .ValueGeneratedOnAdd() 42 | .HasColumnType("TEXT"); 43 | 44 | b.Property("AccessFailedCount") 45 | .HasColumnType("INTEGER"); 46 | 47 | b.Property("ConcurrencyStamp") 48 | .HasColumnType("TEXT"); 49 | 50 | b.Property("Email") 51 | .HasColumnType("TEXT"); 52 | 53 | b.Property("EmailConfirmed") 54 | .HasColumnType("INTEGER"); 55 | 56 | b.Property("LockoutEnabled") 57 | .HasColumnType("INTEGER"); 58 | 59 | b.Property("LockoutEnd") 60 | .HasColumnType("TEXT"); 61 | 62 | b.Property("NormalizedEmail") 63 | .HasColumnType("TEXT"); 64 | 65 | b.Property("NormalizedUserName") 66 | .HasColumnType("TEXT"); 67 | 68 | b.Property("PasswordHash") 69 | .HasColumnType("TEXT"); 70 | 71 | b.Property("PhoneNumber") 72 | .HasColumnType("TEXT"); 73 | 74 | b.Property("PhoneNumberConfirmed") 75 | .HasColumnType("INTEGER"); 76 | 77 | b.Property("SecurityStamp") 78 | .HasColumnType("TEXT"); 79 | 80 | b.Property("TwoFactorEnabled") 81 | .HasColumnType("INTEGER"); 82 | 83 | b.Property("UserName") 84 | .HasColumnType("TEXT"); 85 | 86 | b.HasKey("Id"); 87 | 88 | b.ToTable("Users"); 89 | }); 90 | #pragma warning restore 612, 618 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Migrations/20201211165943_Identity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace AuthenticationServer.API.Migrations 5 | { 6 | public partial class Identity : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.RenameColumn( 11 | name: "Username", 12 | table: "Users", 13 | newName: "UserName"); 14 | 15 | migrationBuilder.AddColumn( 16 | name: "AccessFailedCount", 17 | table: "Users", 18 | type: "INTEGER", 19 | nullable: false, 20 | defaultValue: 0); 21 | 22 | migrationBuilder.AddColumn( 23 | name: "ConcurrencyStamp", 24 | table: "Users", 25 | type: "TEXT", 26 | nullable: true); 27 | 28 | migrationBuilder.AddColumn( 29 | name: "EmailConfirmed", 30 | table: "Users", 31 | type: "INTEGER", 32 | nullable: false, 33 | defaultValue: false); 34 | 35 | migrationBuilder.AddColumn( 36 | name: "LockoutEnabled", 37 | table: "Users", 38 | type: "INTEGER", 39 | nullable: false, 40 | defaultValue: false); 41 | 42 | migrationBuilder.AddColumn( 43 | name: "LockoutEnd", 44 | table: "Users", 45 | type: "TEXT", 46 | nullable: true); 47 | 48 | migrationBuilder.AddColumn( 49 | name: "NormalizedEmail", 50 | table: "Users", 51 | type: "TEXT", 52 | nullable: true); 53 | 54 | migrationBuilder.AddColumn( 55 | name: "NormalizedUserName", 56 | table: "Users", 57 | type: "TEXT", 58 | nullable: true); 59 | 60 | migrationBuilder.AddColumn( 61 | name: "PhoneNumber", 62 | table: "Users", 63 | type: "TEXT", 64 | nullable: true); 65 | 66 | migrationBuilder.AddColumn( 67 | name: "PhoneNumberConfirmed", 68 | table: "Users", 69 | type: "INTEGER", 70 | nullable: false, 71 | defaultValue: false); 72 | 73 | migrationBuilder.AddColumn( 74 | name: "SecurityStamp", 75 | table: "Users", 76 | type: "TEXT", 77 | nullable: true); 78 | 79 | migrationBuilder.AddColumn( 80 | name: "TwoFactorEnabled", 81 | table: "Users", 82 | type: "INTEGER", 83 | nullable: false, 84 | defaultValue: false); 85 | } 86 | 87 | protected override void Down(MigrationBuilder migrationBuilder) 88 | { 89 | migrationBuilder.DropColumn( 90 | name: "AccessFailedCount", 91 | table: "Users"); 92 | 93 | migrationBuilder.DropColumn( 94 | name: "ConcurrencyStamp", 95 | table: "Users"); 96 | 97 | migrationBuilder.DropColumn( 98 | name: "EmailConfirmed", 99 | table: "Users"); 100 | 101 | migrationBuilder.DropColumn( 102 | name: "LockoutEnabled", 103 | table: "Users"); 104 | 105 | migrationBuilder.DropColumn( 106 | name: "LockoutEnd", 107 | table: "Users"); 108 | 109 | migrationBuilder.DropColumn( 110 | name: "NormalizedEmail", 111 | table: "Users"); 112 | 113 | migrationBuilder.DropColumn( 114 | name: "NormalizedUserName", 115 | table: "Users"); 116 | 117 | migrationBuilder.DropColumn( 118 | name: "PhoneNumber", 119 | table: "Users"); 120 | 121 | migrationBuilder.DropColumn( 122 | name: "PhoneNumberConfirmed", 123 | table: "Users"); 124 | 125 | migrationBuilder.DropColumn( 126 | name: "SecurityStamp", 127 | table: "Users"); 128 | 129 | migrationBuilder.DropColumn( 130 | name: "TwoFactorEnabled", 131 | table: "Users"); 132 | 133 | migrationBuilder.RenameColumn( 134 | name: "UserName", 135 | table: "Users", 136 | newName: "Username"); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Migrations/AuthenticationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using AuthenticationServer.API.Models; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | namespace AuthenticationServer.API.Migrations 9 | { 10 | [DbContext(typeof(AuthenticationDbContext))] 11 | partial class AuthenticationDbContextModelSnapshot : ModelSnapshot 12 | { 13 | protected override void BuildModel(ModelBuilder modelBuilder) 14 | { 15 | #pragma warning disable 612, 618 16 | modelBuilder 17 | .HasAnnotation("ProductVersion", "5.0.0"); 18 | 19 | modelBuilder.Entity("AuthenticationServer.API.Models.RefreshToken", b => 20 | { 21 | b.Property("Id") 22 | .ValueGeneratedOnAdd() 23 | .HasColumnType("TEXT"); 24 | 25 | b.Property("Token") 26 | .HasColumnType("TEXT"); 27 | 28 | b.Property("UserId") 29 | .HasColumnType("TEXT"); 30 | 31 | b.HasKey("Id"); 32 | 33 | b.ToTable("RefreshTokens"); 34 | }); 35 | 36 | modelBuilder.Entity("AuthenticationServer.API.Models.User", b => 37 | { 38 | b.Property("Id") 39 | .ValueGeneratedOnAdd() 40 | .HasColumnType("TEXT"); 41 | 42 | b.Property("AccessFailedCount") 43 | .HasColumnType("INTEGER"); 44 | 45 | b.Property("ConcurrencyStamp") 46 | .HasColumnType("TEXT"); 47 | 48 | b.Property("Email") 49 | .HasColumnType("TEXT"); 50 | 51 | b.Property("EmailConfirmed") 52 | .HasColumnType("INTEGER"); 53 | 54 | b.Property("LockoutEnabled") 55 | .HasColumnType("INTEGER"); 56 | 57 | b.Property("LockoutEnd") 58 | .HasColumnType("TEXT"); 59 | 60 | b.Property("NormalizedEmail") 61 | .HasColumnType("TEXT"); 62 | 63 | b.Property("NormalizedUserName") 64 | .HasColumnType("TEXT"); 65 | 66 | b.Property("PasswordHash") 67 | .HasColumnType("TEXT"); 68 | 69 | b.Property("PhoneNumber") 70 | .HasColumnType("TEXT"); 71 | 72 | b.Property("PhoneNumberConfirmed") 73 | .HasColumnType("INTEGER"); 74 | 75 | b.Property("SecurityStamp") 76 | .HasColumnType("TEXT"); 77 | 78 | b.Property("TwoFactorEnabled") 79 | .HasColumnType("INTEGER"); 80 | 81 | b.Property("UserName") 82 | .HasColumnType("TEXT"); 83 | 84 | b.HasKey("Id"); 85 | 86 | b.ToTable("Users"); 87 | }); 88 | #pragma warning restore 612, 618 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Models/AccessToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace AuthenticationServer.API.Models 7 | { 8 | public class AccessToken 9 | { 10 | public string Value { get; set; } 11 | public DateTime ExpirationTime { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Models/AuthenticationConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace AuthenticationServer.API.Models 7 | { 8 | public class AuthenticationConfiguration 9 | { 10 | public string AccessTokenSecret { get; set; } 11 | public double AccessTokenExpirationMinutes { get; set; } 12 | public string Issuer { get; set; } 13 | public string Audience { get; set; } 14 | public string RefreshTokenSecret { get; set; } 15 | public double RefreshTokenExpirationMinutes { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Models/AuthenticationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace AuthenticationServer.API.Models 8 | { 9 | public class AuthenticationDbContext : DbContext 10 | { 11 | public AuthenticationDbContext(DbContextOptions options) : base(options) 12 | { 13 | 14 | } 15 | 16 | public DbSet Users { get; set; } 17 | public DbSet RefreshTokens { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Models/RefreshToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace AuthenticationServer.API.Models 7 | { 8 | public class RefreshToken 9 | { 10 | public Guid Id { get; set; } 11 | public string Token { get; set; } 12 | public Guid UserId { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Models/User.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace AuthenticationServer.API.Models 8 | { 9 | public class User : IdentityUser 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using AuthenticationServer.API.Models; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace AuthenticationServer.API 14 | { 15 | public class Program 16 | { 17 | public static void Main(string[] args) 18 | { 19 | IHost host = CreateHostBuilder(args).Build(); 20 | 21 | using(IServiceScope scope = host.Services.CreateScope()) 22 | { 23 | using (AuthenticationDbContext context = scope.ServiceProvider.GetRequiredService()) 24 | { 25 | context.Database.Migrate(); 26 | } 27 | } 28 | 29 | host.Run(); 30 | } 31 | 32 | public static IHostBuilder CreateHostBuilder(string[] args) => 33 | Host.CreateDefaultBuilder(args) 34 | .ConfigureWebHostDefaults(webBuilder => 35 | { 36 | webBuilder.UseStartup(); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:55964", 7 | "sslPort": 44395 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": false, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "AuthenticationServer.API": { 19 | "commandName": "Project", 20 | "launchBrowser": false, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development", 24 | "AZURE_CLIENT_ID": "", 25 | "AZURE_CLIENT_SECRET": "", 26 | "AZURE_TENANT_ID": "" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Services/Authenticators/Authenticator.cs: -------------------------------------------------------------------------------- 1 | using AuthenticationServer.API.Models; 2 | using AuthenticationServer.API.Services.RefreshTokenRepositories; 3 | using AuthenticationServer.API.Services.TokenGenerators; 4 | using CoreLib.Responses; 5 | using System.Threading.Tasks; 6 | 7 | namespace AuthenticationServer.API.Services.Authenticators 8 | { 9 | public class Authenticator 10 | { 11 | private readonly AccessTokenGenerator _accessTokenGenerator; 12 | private readonly RefreshTokenGenerator _refreshTokenGenerator; 13 | private readonly IRefreshTokenRepository _refreshTokenRepository; 14 | 15 | public Authenticator(AccessTokenGenerator accessTokenGenerator, 16 | RefreshTokenGenerator refreshTokenGenerator, 17 | IRefreshTokenRepository refreshTokenRepository) 18 | { 19 | _accessTokenGenerator = accessTokenGenerator; 20 | _refreshTokenGenerator = refreshTokenGenerator; 21 | _refreshTokenRepository = refreshTokenRepository; 22 | } 23 | 24 | public async Task Authenticate(User user) 25 | { 26 | AccessToken accessToken = _accessTokenGenerator.GenerateToken(user); 27 | string refreshToken = _refreshTokenGenerator.GenerateToken(); 28 | 29 | RefreshToken refreshTokenDTO = new RefreshToken() 30 | { 31 | Token = refreshToken, 32 | UserId = user.Id 33 | }; 34 | await _refreshTokenRepository.Create(refreshTokenDTO); 35 | 36 | return new AuthenticatedUserResponse() 37 | { 38 | AccessToken = accessToken.Value, 39 | AccessTokenExpirationTime = accessToken.ExpirationTime, 40 | RefreshToken = refreshToken 41 | }; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Services/RefreshTokenRepositories/DatabaseRefreshTokenRepository.cs: -------------------------------------------------------------------------------- 1 | using AuthenticationServer.API.Models; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace AuthenticationServer.API.Services.RefreshTokenRepositories 9 | { 10 | public class DatabaseRefreshTokenRepository : IRefreshTokenRepository 11 | { 12 | private readonly AuthenticationDbContext _context; 13 | 14 | public DatabaseRefreshTokenRepository(AuthenticationDbContext context) 15 | { 16 | _context = context; 17 | } 18 | 19 | public async Task Create(RefreshToken refreshToken) 20 | { 21 | _context.RefreshTokens.Add(refreshToken); 22 | await _context.SaveChangesAsync(); 23 | } 24 | 25 | public async Task Delete(Guid id) 26 | { 27 | RefreshToken refreshToken = await _context.RefreshTokens.FindAsync(id); 28 | if(refreshToken != null) 29 | { 30 | _context.RefreshTokens.Remove(refreshToken); 31 | await _context.SaveChangesAsync(); 32 | } 33 | } 34 | 35 | public async Task DeleteAll(Guid userId) 36 | { 37 | IEnumerable refreshTokens = await _context.RefreshTokens 38 | .Where(t => t.UserId == userId) 39 | .ToListAsync(); 40 | 41 | _context.RefreshTokens.RemoveRange(refreshTokens); 42 | await _context.SaveChangesAsync(); 43 | } 44 | 45 | public async Task GetByToken(string token) 46 | { 47 | return await _context.RefreshTokens.FirstOrDefaultAsync(t => t.Token == token); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Services/RefreshTokenRepositories/IRefreshTokenRepository.cs: -------------------------------------------------------------------------------- 1 | using AuthenticationServer.API.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace AuthenticationServer.API.Services.RefreshTokenRepositories 8 | { 9 | public interface IRefreshTokenRepository 10 | { 11 | Task GetByToken(string token); 12 | 13 | Task Create(RefreshToken refreshToken); 14 | 15 | Task Delete(Guid id); 16 | 17 | Task DeleteAll(Guid userId); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Services/RefreshTokenRepositories/InMemoryRefreshTokenRepository.cs: -------------------------------------------------------------------------------- 1 | using AuthenticationServer.API.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace AuthenticationServer.API.Services.RefreshTokenRepositories 8 | { 9 | public class InMemoryRefreshTokenRepository : IRefreshTokenRepository 10 | { 11 | private readonly List _refreshTokens = new List(); 12 | 13 | public Task Create(RefreshToken refreshToken) 14 | { 15 | refreshToken.Id = Guid.NewGuid(); 16 | 17 | _refreshTokens.Add(refreshToken); 18 | 19 | return Task.CompletedTask; 20 | } 21 | 22 | public Task GetByToken(string token) 23 | { 24 | RefreshToken refreshToken = _refreshTokens.FirstOrDefault(r => r.Token == token); 25 | 26 | return Task.FromResult(refreshToken); 27 | } 28 | 29 | public Task Delete(Guid id) 30 | { 31 | _refreshTokens.RemoveAll(r => r.Id == id); 32 | 33 | return Task.CompletedTask; 34 | } 35 | 36 | public Task DeleteAll(Guid userId) 37 | { 38 | _refreshTokens.RemoveAll(r => r.UserId == userId); 39 | 40 | return Task.CompletedTask; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Services/TokenGenerators/AccessTokenGenerator.cs: -------------------------------------------------------------------------------- 1 | using AuthenticationServer.API.Models; 2 | using Microsoft.IdentityModel.Tokens; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IdentityModel.Tokens.Jwt; 6 | using System.Linq; 7 | using System.Security.Claims; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace AuthenticationServer.API.Services.TokenGenerators 12 | { 13 | public class AccessTokenGenerator 14 | { 15 | private readonly AuthenticationConfiguration _configuration; 16 | private readonly TokenGenerator _tokenGenerator; 17 | 18 | public AccessTokenGenerator(AuthenticationConfiguration configuration, TokenGenerator tokenGenerator) 19 | { 20 | _configuration = configuration; 21 | _tokenGenerator = tokenGenerator; 22 | } 23 | 24 | public AccessToken GenerateToken(User user) 25 | { 26 | List claims = new List() 27 | { 28 | new Claim("id", user.Id.ToString()), 29 | new Claim(ClaimTypes.Email, user.Email), 30 | new Claim(ClaimTypes.Name, user.UserName), 31 | }; 32 | 33 | DateTime expirationTime = DateTime.UtcNow.AddMinutes(_configuration.AccessTokenExpirationMinutes); 34 | string value = _tokenGenerator.GenerateToken( 35 | _configuration.AccessTokenSecret, 36 | _configuration.Issuer, 37 | _configuration.Audience, 38 | expirationTime, 39 | claims); 40 | 41 | return new AccessToken() 42 | { 43 | Value = value, 44 | ExpirationTime = expirationTime 45 | }; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Services/TokenGenerators/RefreshTokenGenerator.cs: -------------------------------------------------------------------------------- 1 | using AuthenticationServer.API.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace AuthenticationServer.API.Services.TokenGenerators 8 | { 9 | public class RefreshTokenGenerator 10 | { 11 | private readonly AuthenticationConfiguration _configuration; 12 | private readonly TokenGenerator _tokenGenerator; 13 | 14 | public RefreshTokenGenerator(AuthenticationConfiguration configuration, TokenGenerator tokenGenerator) 15 | { 16 | _configuration = configuration; 17 | _tokenGenerator = tokenGenerator; 18 | } 19 | 20 | public string GenerateToken() 21 | { 22 | DateTime expirationTime = DateTime.UtcNow.AddMinutes(_configuration.RefreshTokenExpirationMinutes); 23 | 24 | return _tokenGenerator.GenerateToken( 25 | _configuration.RefreshTokenSecret, 26 | _configuration.Issuer, 27 | _configuration.Audience, 28 | expirationTime); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Services/TokenGenerators/TokenGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.IdentityModel.Tokens; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IdentityModel.Tokens.Jwt; 5 | using System.Linq; 6 | using System.Security.Claims; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace AuthenticationServer.API.Services.TokenGenerators 11 | { 12 | public class TokenGenerator 13 | { 14 | public string GenerateToken(string secretKey, string issuer, string audience, DateTime utcExpirationTime, 15 | IEnumerable claims = null) 16 | { 17 | SecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); 18 | SigningCredentials credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); 19 | 20 | JwtSecurityToken token = new JwtSecurityToken( 21 | issuer, 22 | audience, 23 | claims, 24 | DateTime.UtcNow, 25 | utcExpirationTime, 26 | credentials); 27 | 28 | return new JwtSecurityTokenHandler().WriteToken(token); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Services/TokenValidators/RefreshTokenValidator.cs: -------------------------------------------------------------------------------- 1 | using AuthenticationServer.API.Models; 2 | using Microsoft.IdentityModel.Tokens; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IdentityModel.Tokens.Jwt; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace AuthenticationServer.API.Services.TokenValidators 11 | { 12 | public class RefreshTokenValidator 13 | { 14 | private readonly AuthenticationConfiguration _configuration; 15 | 16 | public RefreshTokenValidator(AuthenticationConfiguration configuration) 17 | { 18 | _configuration = configuration; 19 | } 20 | 21 | public bool Validate(string refreshToken) 22 | { 23 | TokenValidationParameters validationParameters = new TokenValidationParameters() 24 | { 25 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.RefreshTokenSecret)), 26 | ValidIssuer = _configuration.Issuer, 27 | ValidAudience = _configuration.Audience, 28 | ValidateIssuerSigningKey = true, 29 | ValidateIssuer = true, 30 | ValidateAudience = true, 31 | ClockSkew = TimeSpan.Zero 32 | }; 33 | 34 | JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); 35 | 36 | try 37 | { 38 | tokenHandler.ValidateToken(refreshToken, validationParameters, out SecurityToken validatedToken); 39 | return true; 40 | } 41 | catch (Exception) 42 | { 43 | return false; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /AuthenticationServer.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using AuthenticationServer.API.Models; 4 | using AuthenticationServer.API.Services.Authenticators; 5 | using AuthenticationServer.API.Services.RefreshTokenRepositories; 6 | using AuthenticationServer.API.Services.TokenGenerators; 7 | using AuthenticationServer.API.Services.TokenValidators; 8 | using Azure.Identity; 9 | using Azure.Security.KeyVault.Secrets; 10 | using Microsoft.AspNetCore.Authentication.JwtBearer; 11 | using Microsoft.AspNetCore.Builder; 12 | using Microsoft.AspNetCore.Hosting; 13 | using Microsoft.EntityFrameworkCore; 14 | using Microsoft.Extensions.Configuration; 15 | using Microsoft.Extensions.DependencyInjection; 16 | using Microsoft.Extensions.Hosting; 17 | using Microsoft.IdentityModel.Tokens; 18 | 19 | namespace AuthenticationServer.API 20 | { 21 | public class Startup 22 | { 23 | private readonly IConfiguration _configuration; 24 | 25 | public Startup(IConfiguration configuration) 26 | { 27 | _configuration = configuration; 28 | } 29 | 30 | public void ConfigureServices(IServiceCollection services) 31 | { 32 | services.AddControllers(); 33 | 34 | services.AddIdentityCore(o => 35 | { 36 | o.User.RequireUniqueEmail = true; 37 | 38 | o.Password.RequireDigit = false; 39 | o.Password.RequireNonAlphanumeric = false; 40 | o.Password.RequireUppercase = false; 41 | o.Password.RequiredLength = 0; 42 | }).AddEntityFrameworkStores(); 43 | 44 | AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); 45 | _configuration.Bind("Authentication", authenticationConfiguration); 46 | 47 | //SecretClient keyVaultClient = new SecretClient( 48 | // new Uri(_configuration.GetValue("KeyVaultUri")), 49 | // new DefaultAzureCredential()); 50 | //authenticationConfiguration.AccessTokenSecret = keyVaultClient.GetSecret("access-token-secret").Value.Value; 51 | 52 | services.AddSingleton(authenticationConfiguration); 53 | 54 | string connectionString = _configuration.GetConnectionString("sqlite"); 55 | services.AddDbContext(o => o.UseSqlite(connectionString)); 56 | 57 | services.AddSingleton(); 58 | services.AddSingleton(); 59 | services.AddSingleton(); 60 | services.AddScoped(); 61 | services.AddSingleton(); 62 | services.AddScoped(); 63 | 64 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o => 65 | { 66 | o.TokenValidationParameters = new TokenValidationParameters() 67 | { 68 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authenticationConfiguration.AccessTokenSecret)), 69 | ValidIssuer = authenticationConfiguration.Issuer, 70 | ValidAudience = authenticationConfiguration.Audience, 71 | ValidateIssuerSigningKey = true, 72 | ValidateIssuer = true, 73 | ValidateAudience = true, 74 | ClockSkew = TimeSpan.Zero 75 | }; 76 | }); 77 | } 78 | 79 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 80 | { 81 | if (env.IsDevelopment()) 82 | { 83 | app.UseDeveloperExceptionPage(); 84 | } 85 | 86 | app.UseRouting(); 87 | 88 | app.UseAuthentication(); 89 | app.UseAuthorization(); 90 | 91 | app.UseEndpoints(endpoints => 92 | { 93 | endpoints.MapControllers(); 94 | }); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /AuthenticationServer.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /AuthenticationServer.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "KeyVaultUri": "https://auth-server-ss.vault.azure.net/", 11 | "Authentication": { 12 | "AccessTokenSecret": "4y7XS2AHicSOs2uUJCxwlHWqTJNExW3UDUjMeXi96uLEso1YV4RazqQubpFBdx0zZGtdxBelKURhh0WXxPR0mEJQHk_0U9HeYtqcMManhoP3X2Ge8jgxh6k4C_Gd4UPTc6lkx0Ca5eRE16ciFQ6wmYDnaXC8NbngGqartHccAxE", 13 | "RefreshTokenSecret": "3y7XS2AHicSOs2uUJCxwlHWqTJNExW3UDUjMeXi96uLEso1YV4RazqQubpFBdx0zZGtdxBelKURhh0WXxPR0mEJQHk_0U9HeYtqcMManhoP3X2Ge8jgxh6k4C_Gd4UPTc6lkx0Ca5eRE16ciFQ6wmYDnaXC8NbngGqartHccAxE", 14 | "AccessTokenExpirationMinutes": "15", 15 | "RefreshTokenExpirationMinutes": "131400", 16 | "Issuer": "https://localhost:5001", 17 | "Audience": "https://localhost:5001" 18 | }, 19 | "ConnectionStrings": { 20 | "sqlite": "Data Source=Authentication.db" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AuthenticationServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30503.244 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AuthenticationServer.API", "AuthenticationServer.API\AuthenticationServer.API.csproj", "{57C0E66F-A1B6-4EFB-884E-64514F01E2CF}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataServer.API", "DataServer.API\DataServer.API.csproj", "{50E31A1D-389B-4252-BD1E-BFD5EC0523E0}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientApplication", "ClientApplication\ClientApplication.csproj", "{DD4F220E-C0FB-463C-9710-5C5182ABB872}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreLib", "Core\CoreLib.csproj", "{820B0153-643F-4123-8A37-11A73BEEA9C1}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {57C0E66F-A1B6-4EFB-884E-64514F01E2CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {57C0E66F-A1B6-4EFB-884E-64514F01E2CF}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {57C0E66F-A1B6-4EFB-884E-64514F01E2CF}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {57C0E66F-A1B6-4EFB-884E-64514F01E2CF}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {50E31A1D-389B-4252-BD1E-BFD5EC0523E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {50E31A1D-389B-4252-BD1E-BFD5EC0523E0}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {50E31A1D-389B-4252-BD1E-BFD5EC0523E0}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {50E31A1D-389B-4252-BD1E-BFD5EC0523E0}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {DD4F220E-C0FB-463C-9710-5C5182ABB872}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {DD4F220E-C0FB-463C-9710-5C5182ABB872}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {DD4F220E-C0FB-463C-9710-5C5182ABB872}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {DD4F220E-C0FB-463C-9710-5C5182ABB872}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {820B0153-643F-4123-8A37-11A73BEEA9C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {820B0153-643F-4123-8A37-11A73BEEA9C1}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {820B0153-643F-4123-8A37-11A73BEEA9C1}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {820B0153-643F-4123-8A37-11A73BEEA9C1}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {22080EC3-FAA9-448A-B298-F86B819E92D0} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /ClientApplication/ClientApplication.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ClientApplication/Http/AutoRefreshHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using ClientApplication.Services; 2 | using ClientApplication.Stores; 3 | using CoreLib.Requests; 4 | using CoreLib.Responses; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net.Http; 9 | using System.Net.Http.Headers; 10 | using System.Text; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | namespace ClientApplication.Http 15 | { 16 | public class AutoRefreshHttpMessageHandler : DelegatingHandler 17 | { 18 | private readonly TokenStore _tokenStore; 19 | private readonly IRefreshService _refreshService; 20 | 21 | public AutoRefreshHttpMessageHandler(TokenStore tokenStore, IRefreshService refreshService) : base(new HttpClientHandler()) 22 | { 23 | _tokenStore = tokenStore; 24 | _refreshService = refreshService; 25 | } 26 | 27 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 28 | { 29 | if (IsAccessTokenExpired()) 30 | { 31 | await RefreshAccessToken(); 32 | } 33 | 34 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _tokenStore.AccessToken); 35 | 36 | return await base.SendAsync(request, cancellationToken); 37 | } 38 | 39 | private bool IsAccessTokenExpired() 40 | { 41 | return _tokenStore.AccessTokenExpirationTime <= DateTime.UtcNow; 42 | } 43 | 44 | private async Task RefreshAccessToken() 45 | { 46 | AuthenticatedUserResponse authenticatedUserResponse = await _refreshService.Refresh(new RefreshRequest 47 | { 48 | RefreshToken = _tokenStore.RefreshToken 49 | }); 50 | 51 | _tokenStore.AccessToken = authenticatedUserResponse.AccessToken; 52 | _tokenStore.AccessTokenExpirationTime = authenticatedUserResponse.AccessTokenExpirationTime; 53 | _tokenStore.RefreshToken = authenticatedUserResponse.RefreshToken; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ClientApplication/Program.cs: -------------------------------------------------------------------------------- 1 | using ClientApplication.Http; 2 | using ClientApplication.Services; 3 | using ClientApplication.Stores; 4 | using CoreLib.Requests; 5 | using CoreLib.Responses; 6 | using Refit; 7 | using System; 8 | using System.Linq; 9 | using System.Net.Http; 10 | using System.Threading.Tasks; 11 | 12 | namespace ClientApplication 13 | { 14 | class Program 15 | { 16 | static async Task Main(string[] args) 17 | { 18 | string authenticationBaseUrl = "http://localhost:5000"; 19 | string dataBaseUrl = "http://localhost:8000"; 20 | 21 | IRefreshService refreshService = RestService.For(authenticationBaseUrl); 22 | TokenStore tokenStore = new TokenStore(); 23 | AutoRefreshHttpMessageHandler autoRefreshHttpMessageHandler = new AutoRefreshHttpMessageHandler(tokenStore, refreshService); 24 | 25 | IRegisterService registerService = RestService.For(authenticationBaseUrl); 26 | ILoginService loginService = RestService.For(authenticationBaseUrl); 27 | IDataService dataService = RestService.For(new HttpClient(autoRefreshHttpMessageHandler) 28 | { 29 | BaseAddress = new Uri(dataBaseUrl) 30 | }); 31 | 32 | try 33 | { 34 | await registerService.Register(new RegisterRequest 35 | { 36 | Email = "test@gmail.com", 37 | Username = "SingletonSean", 38 | Password = "test123", 39 | ConfirmPassword = "test123" 40 | }); 41 | } 42 | catch (ApiException ex) 43 | { 44 | ErrorResponse errorResponse = await ex.GetContentAsAsync(); 45 | Console.WriteLine(errorResponse.ErrorMessages.FirstOrDefault()); 46 | } 47 | 48 | AuthenticatedUserResponse loginResponse = await loginService.Login(new LoginRequest() 49 | { 50 | Username = "SingletonSean", 51 | Password = "test123", 52 | }); 53 | tokenStore.AccessToken = loginResponse.AccessToken; 54 | tokenStore.AccessTokenExpirationTime = loginResponse.AccessTokenExpirationTime; 55 | tokenStore.RefreshToken = loginResponse.RefreshToken; 56 | 57 | DataResponse dataResponse = await dataService.GetData(); 58 | 59 | Console.ReadKey(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ClientApplication/Services/IDataService.cs: -------------------------------------------------------------------------------- 1 | using CoreLib.Responses; 2 | using Refit; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace ClientApplication.Services 10 | { 11 | public interface IDataService 12 | { 13 | [Get("/data")] 14 | Task GetData(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ClientApplication/Services/ILoginService.cs: -------------------------------------------------------------------------------- 1 | using CoreLib.Requests; 2 | using CoreLib.Responses; 3 | using Refit; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace ClientApplication.Services 11 | { 12 | public interface ILoginService 13 | { 14 | [Post("/login")] 15 | Task Login([Body] LoginRequest loginRequest); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ClientApplication/Services/IRefreshService.cs: -------------------------------------------------------------------------------- 1 | using CoreLib.Requests; 2 | using CoreLib.Responses; 3 | using Refit; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace ClientApplication.Services 11 | { 12 | public interface IRefreshService 13 | { 14 | [Post("/refresh")] 15 | Task Refresh([Body] RefreshRequest refreshRequest); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ClientApplication/Services/IRegisterService.cs: -------------------------------------------------------------------------------- 1 | using CoreLib.Requests; 2 | using Refit; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace ClientApplication.Services 10 | { 11 | public interface IRegisterService 12 | { 13 | [Post("/register")] 14 | Task Register([Body] RegisterRequest registerRequest); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ClientApplication/Stores/TokenStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace ClientApplication.Stores 8 | { 9 | public class TokenStore 10 | { 11 | public string AccessToken { get; set; } 12 | public DateTime AccessTokenExpirationTime { get; set; } 13 | public string RefreshToken { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Core/CoreLib.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Core/Requests/LoginRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace CoreLib.Requests 8 | { 9 | public class LoginRequest 10 | { 11 | [Required] 12 | public string Username { get; set; } 13 | 14 | [Required] 15 | public string Password { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Core/Requests/RefreshRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace CoreLib.Requests 8 | { 9 | public class RefreshRequest 10 | { 11 | [Required] 12 | public string RefreshToken { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Core/Requests/RegisterRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace CoreLib.Requests 8 | { 9 | public class RegisterRequest 10 | { 11 | [Required] 12 | [EmailAddress] 13 | public string Email { get; set; } 14 | 15 | [Required] 16 | public string Username { get; set; } 17 | 18 | [Required] 19 | public string Password { get; set; } 20 | 21 | [Required] 22 | public string ConfirmPassword { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Core/Responses/AuthenticatedUserResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CoreLib.Responses 7 | { 8 | public class AuthenticatedUserResponse 9 | { 10 | public string AccessToken { get; set; } 11 | public DateTime AccessTokenExpirationTime { get; set; } 12 | public string RefreshToken { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Core/Responses/DataResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace CoreLib.Responses 6 | { 7 | public class DataResponse 8 | { 9 | public string Value { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Core/Responses/ErrorResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace CoreLib.Responses 7 | { 8 | public class ErrorResponse 9 | { 10 | public IEnumerable ErrorMessages { get; set; } 11 | 12 | public ErrorResponse() : this(new List()) { } 13 | 14 | public ErrorResponse(string errorMessage) : this(new List() { errorMessage }) { } 15 | 16 | public ErrorResponse(IEnumerable errorMessages) 17 | { 18 | ErrorMessages = errorMessages; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DataServer.API/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using System.Threading.Tasks; 6 | using CoreLib.Responses; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.AspNetCore.Mvc; 9 | 10 | namespace DataServer.API.Controllers 11 | { 12 | public class HomeController : Controller 13 | { 14 | [Authorize] 15 | [HttpGet("data")] 16 | public IActionResult Index() 17 | { 18 | string id = HttpContext.User.FindFirstValue("id"); 19 | string email = HttpContext.User.FindFirstValue(ClaimTypes.Email); 20 | string username = HttpContext.User.FindFirstValue(ClaimTypes.Name); 21 | 22 | return Ok(new DataResponse 23 | { 24 | Value = "123" 25 | }); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DataServer.API/DataServer.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /DataServer.API/DataServer.API.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | MvcControllerEmptyScaffolder 5 | root/Controller 6 | DataServer.API 7 | 8 | 9 | ProjectDebugger 10 | 11 | -------------------------------------------------------------------------------- /DataServer.API/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace DataServer.API 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /DataServer.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:58887", 8 | "sslPort": 44324 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": false, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "DataServer.API": { 21 | "commandName": "Project", 22 | "launchBrowser": false, 23 | "launchUrl": "weatherforecast", 24 | "applicationUrl": "https://localhost:8001;http://localhost:8000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development", 27 | "AZURE_CLIENT_ID": "", 28 | "AZURE_CLIENT_SECRET": "", 29 | "AZURE_TENANT_ID": "" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DataServer.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Azure.Identity; 7 | using Azure.Security.KeyVault.Secrets; 8 | using Microsoft.AspNetCore.Authentication.JwtBearer; 9 | using Microsoft.AspNetCore.Builder; 10 | using Microsoft.AspNetCore.Hosting; 11 | using Microsoft.AspNetCore.HttpsPolicy; 12 | using Microsoft.AspNetCore.Mvc; 13 | using Microsoft.Extensions.Configuration; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using Microsoft.Extensions.Hosting; 16 | using Microsoft.Extensions.Logging; 17 | using Microsoft.IdentityModel.Tokens; 18 | 19 | namespace DataServer.API 20 | { 21 | public class Startup 22 | { 23 | public Startup(IConfiguration configuration) 24 | { 25 | Configuration = configuration; 26 | } 27 | 28 | public IConfiguration Configuration { get; } 29 | 30 | public void ConfigureServices(IServiceCollection services) 31 | { 32 | services.AddControllers(); 33 | 34 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(o => 35 | { 36 | AuthenticationConfiguration authenticationConfiguration = new AuthenticationConfiguration(); 37 | 38 | //SecretClient keyVaultClient = new SecretClient( 39 | // new Uri(Configuration.GetValue("KeyVaultUri")), 40 | // new DefaultAzureCredential()); 41 | //authenticationConfiguration.AccessTokenSecret = keyVaultClient.GetSecret("access-token-secret").Value.Value; 42 | 43 | Configuration.Bind("Authentication", authenticationConfiguration); 44 | 45 | o.TokenValidationParameters = new TokenValidationParameters() 46 | { 47 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authenticationConfiguration.AccessTokenSecret)), 48 | ValidIssuer = authenticationConfiguration.Issuer, 49 | ValidAudience = authenticationConfiguration.Audience, 50 | ValidateIssuerSigningKey = true, 51 | ValidateIssuer = true, 52 | ValidateAudience = true, 53 | ClockSkew = TimeSpan.Zero 54 | }; 55 | }); 56 | } 57 | 58 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 59 | { 60 | if (env.IsDevelopment()) 61 | { 62 | app.UseDeveloperExceptionPage(); 63 | } 64 | 65 | app.UseRouting(); 66 | 67 | app.UseAuthentication(); 68 | app.UseAuthorization(); 69 | 70 | app.UseEndpoints(endpoints => 71 | { 72 | endpoints.MapControllers(); 73 | }); 74 | } 75 | 76 | private class AuthenticationConfiguration 77 | { 78 | public string AccessTokenSecret { get; set; } 79 | public string Issuer { get; set; } 80 | public string Audience { get; set; } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /DataServer.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /DataServer.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "KeyVaultUri": "https://authserver.vault.azure.net/", 11 | "Authentication": { 12 | "AccessTokenSecret": "4y7XS2AHicSOs2uUJCxwlHWqTJNExW3UDUjMeXi96uLEso1YV4RazqQubpFBdx0zZGtdxBelKURhh0WXxPR0mEJQHk_0U9HeYtqcMManhoP3X2Ge8jgxh6k4C_Gd4UPTc6lkx0Ca5eRE16ciFQ6wmYDnaXC8NbngGqartHccAxE", 13 | "Issuer": "https://localhost:5001", 14 | "Audience": "https://localhost:5001" 15 | } 16 | } 17 | --------------------------------------------------------------------------------