diff options
author | crupest <crupest@outlook.com> | 2019-02-07 00:39:51 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2019-02-07 00:39:51 +0800 |
commit | 101631a0041f22570d7c2d9378cbfd0cec5ca14b (patch) | |
tree | 93a50ac104e1f590f9d7aa3d8f0e140a0992035d | |
parent | 478dfefdbf4b118d7453673a3aa93a638586b850 (diff) | |
download | timeline-101631a0041f22570d7c2d9378cbfd0cec5ca14b.tar.gz timeline-101631a0041f22570d7c2d9378cbfd0cec5ca14b.tar.bz2 timeline-101631a0041f22570d7c2d9378cbfd0cec5ca14b.zip |
Add authorization.
-rw-r--r-- | Timeline.Tests/AuthorizationUnitTest.cs | 94 | ||||
-rw-r--r-- | Timeline.Tests/UnitTest.cs | 58 | ||||
-rw-r--r-- | Timeline/Controllers/SampleDataController.cs | 46 | ||||
-rw-r--r-- | Timeline/Controllers/TestController.cs | 34 | ||||
-rw-r--r-- | Timeline/Controllers/UserController.cs | 46 | ||||
-rw-r--r-- | Timeline/Entities/User.cs | 1 | ||||
-rw-r--r-- | Timeline/Properties/launchSettings.json | 3 | ||||
-rw-r--r-- | Timeline/Services/JwtService.cs | 64 | ||||
-rw-r--r-- | Timeline/Services/UserService.cs | 4 | ||||
-rw-r--r-- | Timeline/Startup.cs | 16 | ||||
-rw-r--r-- | Timeline/Timeline.csproj | 1 | ||||
-rw-r--r-- | Timeline/appsettings.json | 1 |
12 files changed, 219 insertions, 149 deletions
diff --git a/Timeline.Tests/AuthorizationUnitTest.cs b/Timeline.Tests/AuthorizationUnitTest.cs new file mode 100644 index 00000000..e9e86c8e --- /dev/null +++ b/Timeline.Tests/AuthorizationUnitTest.cs @@ -0,0 +1,94 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Newtonsoft.Json; +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Timeline.Controllers; +using Timeline.Tests.Helpers; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests +{ + public class AuthorizationUnitTest : IClassFixture<WebApplicationFactory<Startup>> + { + private readonly WebApplicationFactory<Startup> _factory; + + public AuthorizationUnitTest(WebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper) + { + _factory = factory.WithTestConfig(outputHelper); + } + + [Fact] + public async Task UnauthenticationTest() + { + using (var client = _factory.CreateDefaultClient()) + { + var response = await client.GetAsync("/api/Test/Action1"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + } + + private static async Task<string> Login(HttpClient client, string username, string password) + { + var response = await client.PostAsJsonAsync("/api/User/LogIn", new UserController.UserCredentials { Username = username, Password = password }); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var loginInfo = JsonConvert.DeserializeObject<UserController.LoginInfo>(await response.Content.ReadAsStringAsync()); + + return loginInfo.Token; + } + + private static async Task<HttpResponseMessage> GetWithAuthentication(HttpClient client, string path, string token) + { + var request = new HttpRequestMessage + { + RequestUri = new Uri(client.BaseAddress, path), + Method = HttpMethod.Get + }; + request.Headers.Add("Authorization", "Bearer " + token); + + return await client.SendAsync(request); + } + + [Fact] + public async Task AuthenticationTest() + { + using (var client = _factory.CreateDefaultClient()) + { + var token = await Login(client, "user", "user"); + var response = await GetWithAuthentication(client, "/api/Test/Action1", token); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + } + + [Fact] + public async Task UserAuthorizationTest() + { + using (var client = _factory.CreateDefaultClient()) + { + var token = await Login(client, "user", "user"); + var response1 = await GetWithAuthentication(client, "/api/Test/Action2", token); + Assert.Equal(HttpStatusCode.OK, response1.StatusCode); + var response2 = await GetWithAuthentication(client, "/api/Test/Action3", token); + Assert.Equal(HttpStatusCode.Forbidden, response2.StatusCode); + } + } + + [Fact] + public async Task AdminAuthorizationTest() + { + using (var client = _factory.CreateDefaultClient()) + { + var token = await Login(client, "admin", "admin"); + var response1 = await GetWithAuthentication(client, "/api/Test/Action2", token); + Assert.Equal(HttpStatusCode.OK, response1.StatusCode); + var response2 = await GetWithAuthentication(client, "/api/Test/Action3", token); + Assert.Equal(HttpStatusCode.OK, response2.StatusCode); + } + } + } +} diff --git a/Timeline.Tests/UnitTest.cs b/Timeline.Tests/UnitTest.cs deleted file mode 100644 index c9ff7296..00000000 --- a/Timeline.Tests/UnitTest.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Testing; -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Timeline.Controllers; -using Timeline.Tests.Helpers; -using Xunit; -using Xunit.Abstractions; - -namespace Timeline.Tests -{ - public class UnitTest : IClassFixture<WebApplicationFactory<Startup>> - { - private readonly WebApplicationFactory<Startup> _factory; - - public UnitTest(WebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper) - { - _factory = factory.WithTestConfig(outputHelper); - } - - [Fact] - public async Task UnauthenticationTest() - { - using (var client = _factory.CreateDefaultClient()) - { - var response = await client.GetAsync("/api/SampleData/WeatherForecasts"); - - Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); - } - } - - [Fact] - public async Task AuthenticationTest() - { - using (var client = _factory.CreateDefaultClient()) - { - var response = await client.PostAsJsonAsync("/api/User/LogIn", new UserController.UserCredentials { Username = "hello", Password = "crupest" }); - - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var token = response.Headers.GetValues("Authorization").Single(); - - var request = new HttpRequestMessage - { - RequestUri = new Uri(client.BaseAddress, "/api/SampleData/WeatherForecasts"), - Method = HttpMethod.Get - }; - request.Headers.Add("Authorization", token); - - var response2 = await client.SendAsync(request); - - Assert.Equal(HttpStatusCode.OK, response2.StatusCode); - } - } - } -} diff --git a/Timeline/Controllers/SampleDataController.cs b/Timeline/Controllers/SampleDataController.cs deleted file mode 100644 index 04e7f127..00000000 --- a/Timeline/Controllers/SampleDataController.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Timeline.Controllers -{ - [Route("api/[controller]")] - public class SampleDataController : Controller - { - private static string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - [HttpGet("[action]")] - [Authorize] - public IEnumerable<WeatherForecast> WeatherForecasts() - { - var rng = new Random(); - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - DateFormatted = DateTime.Now.AddDays(index).ToString("d"), - TemperatureC = rng.Next(-20, 55), - Summary = Summaries[rng.Next(Summaries.Length)] - }); - } - - public class WeatherForecast - { - public string DateFormatted { get; set; } - public int TemperatureC { get; set; } - public string Summary { get; set; } - - public int TemperatureF - { - get - { - return 32 + (int)(TemperatureC / 0.5556); - } - } - } - } -} diff --git a/Timeline/Controllers/TestController.cs b/Timeline/Controllers/TestController.cs new file mode 100644 index 00000000..1563830c --- /dev/null +++ b/Timeline/Controllers/TestController.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Timeline.Controllers +{ + [Route("api/[controller]")] + public class TestController : Controller + { + [HttpGet("[action]")] + [Authorize] + public string Action1() + { + return "test"; + } + + [HttpGet("[action]")] + [Authorize(Roles = "User,Admin")] + public string Action2() + { + return "test"; + } + + [HttpGet("[action]")] + [Authorize(Roles = "Admin")] + public string Action3() + { + return "test"; + } + } +} diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 08f9a66a..9d6970e7 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,15 +1,6 @@ -using System; -using System.IdentityModel.Tokens.Jwt; -using System.Linq; -using System.Security.Claims; -using System.Text; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using Timeline.Configs; using Timeline.Services; namespace Timeline.Controllers @@ -29,20 +20,26 @@ namespace Timeline.Controllers public string Password { get; set; } } - private readonly IOptionsMonitor<JwtConfig> _jwtConfig; + public class LoginInfo + { + public string Token { get; set; } + public string[] Roles { get; set; } + } + private readonly IUserService _userService; + private readonly IJwtService _jwtService; private readonly ILogger<UserController> _logger; - public UserController(IOptionsMonitor<JwtConfig> jwtConfig, IUserService userService, ILogger<UserController> logger) + public UserController(IUserService userService, IJwtService jwtService, ILogger<UserController> logger) { - _jwtConfig = jwtConfig; _userService = userService; + _jwtService = jwtService; _logger = logger; } [HttpPost("[action]")] [AllowAnonymous] - public IActionResult LogIn([FromBody] UserCredentials credentials) + public ActionResult<LoginInfo> LogIn([FromBody] UserCredentials credentials) { var user = _userService.Authenticate(credentials.Username, credentials.Password); @@ -51,28 +48,15 @@ namespace Timeline.Controllers return BadRequest(); } - _logger.LogInformation(LoggingEventIds.LogInSucceeded, "Login with username: {} succeeded."); + _logger.LogInformation(LoggingEventIds.LogInSucceeded, "Login with username: {} succeeded.", credentials.Username); - var jwtConfig = _jwtConfig.CurrentValue; - - var handler = new JwtSecurityTokenHandler(); - var tokenDescriptor = new SecurityTokenDescriptor() + var result = new LoginInfo { - Subject = new ClaimsIdentity(new Claim[]{ new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) }), - Issuer = jwtConfig.Issuer, - Audience = jwtConfig.Audience, - SigningCredentials = new SigningCredentials( - new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.SigningKey)), SecurityAlgorithms.HmacSha384), - IssuedAt = DateTime.Now, - Expires = DateTime.Now.AddDays(1) + Token = _jwtService.GenerateJwtToken(user), + Roles = user.Roles }; - var token = handler.CreateToken(tokenDescriptor); - var tokenString = handler.WriteToken(token); - - Response.Headers.Append("Authorization", "Bearer " + tokenString); - - return Ok(); + return Ok(result); } } } diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/User.cs index 87c38c32..3f7e9ac6 100644 --- a/Timeline/Entities/User.cs +++ b/Timeline/Entities/User.cs @@ -10,5 +10,6 @@ namespace Timeline.Entities public int Id { get; set; } public string Username { get; set; } public string Password { get; set; } + public string[] Roles { get; set; } } } diff --git a/Timeline/Properties/launchSettings.json b/Timeline/Properties/launchSettings.json index d5e76a2a..a07a7868 100644 --- a/Timeline/Properties/launchSettings.json +++ b/Timeline/Properties/launchSettings.json @@ -20,8 +20,7 @@ "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "" + } } } }
\ No newline at end of file diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs new file mode 100644 index 00000000..1b465dd9 --- /dev/null +++ b/Timeline/Services/JwtService.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Timeline.Configs; +using Timeline.Entities; + +namespace Timeline.Services +{ + public interface IJwtService + { + /// <summary> + /// Create a JWT token for a given user. + /// Return null if <paramref name="user"/> is null. + /// </summary> + /// <param name="user">The user to generate token.</param> + /// <returns>The generated token or null if <paramref name="user"/> is null.</returns> + string GenerateJwtToken(User user); + } + + public class JwtService : IJwtService + { + private readonly IOptionsMonitor<JwtConfig> _jwtConfig; + private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); + + public JwtService(IOptionsMonitor<JwtConfig> jwtConfig) + { + _jwtConfig = jwtConfig; + } + + public string GenerateJwtToken(User user) + { + if (user == null) + return null; + + var jwtConfig = _jwtConfig.CurrentValue; + + var identity = new ClaimsIdentity(); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())); + identity.AddClaims(user.Roles.Select(role => new Claim(identity.RoleClaimType, role))); + + var tokenDescriptor = new SecurityTokenDescriptor() + { + Subject = identity, + Issuer = jwtConfig.Issuer, + Audience = jwtConfig.Audience, + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.SigningKey)), SecurityAlgorithms.HmacSha384), + IssuedAt = DateTime.Now, + Expires = DateTime.Now.AddDays(1) + }; + + var token = _tokenHandler.CreateToken(tokenDescriptor); + var tokenString = _tokenHandler.WriteToken(token); + + return tokenString; + } + } +} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index b3d76e3e..ab5a31bb 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -21,8 +21,8 @@ namespace Timeline.Services public class UserService : IUserService { private readonly IList<User> _users = new List<User>{ - new User { Id = 0, Username = "hello", Password = "crupest" }, - new User { Id = 1, Username = "test", Password = "test"} + new User { Id = 0, Username = "admin", Password = "admin", Roles = new string[] { "User", "Admin" } }, + new User { Id = 1, Username = "user", Password = "user", Roles = new string[] { "User"} } }; public User Authenticate(string username, string password) diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 6dde227f..2a0f437f 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -9,6 +9,7 @@ using Microsoft.IdentityModel.Tokens; using System.Text; using Timeline.Configs; using Timeline.Services; +using Microsoft.AspNetCore.HttpOverrides; namespace Timeline { @@ -49,6 +50,7 @@ namespace Timeline }); services.AddSingleton<IUserService, UserService>(); + services.AddSingleton<IJwtService, JwtService>(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -61,20 +63,16 @@ namespace Timeline else { app.UseExceptionHandler("/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); } - - // When not in test environment, use https redirection. - // Because I found some problems using https redirection in test mode. - if (!env.IsTest()) - app.UseHttpsRedirection(); - - app.UseStaticFiles(); app.UseSpaStaticFiles(); + app.UseForwardedHeaders(new ForwardedHeadersOptions + { + ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto + }); + app.UseAuthentication(); app.UseMvc(routes => diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 9be6893a..37ca698d 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -11,6 +11,7 @@ <!-- Set this to true if you enable server-side prerendering --> <BuildServerSideRenderer>false</BuildServerSideRenderer> <UserSecretsId>1f6fb74d-4277-4bc0-aeea-b1fc5ffb0b43</UserSecretsId> + <Authors>crupest</Authors> </PropertyGroup> <ItemGroup> diff --git a/Timeline/appsettings.json b/Timeline/appsettings.json index 75395c71..a914603c 100644 --- a/Timeline/appsettings.json +++ b/Timeline/appsettings.json @@ -4,7 +4,6 @@ "Default": "Warning" } }, - "AllowedHosts": "*", "JwtConfig": { "Issuer": "crupest.xyz", "Audience": "crupest.xyz" |