aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Timeline.Tests/AuthorizationUnitTest.cs94
-rw-r--r--Timeline.Tests/UnitTest.cs58
-rw-r--r--Timeline/Controllers/SampleDataController.cs46
-rw-r--r--Timeline/Controllers/TestController.cs34
-rw-r--r--Timeline/Controllers/UserController.cs46
-rw-r--r--Timeline/Entities/User.cs1
-rw-r--r--Timeline/Properties/launchSettings.json3
-rw-r--r--Timeline/Services/JwtService.cs64
-rw-r--r--Timeline/Services/UserService.cs4
-rw-r--r--Timeline/Startup.cs16
-rw-r--r--Timeline/Timeline.csproj1
-rw-r--r--Timeline/appsettings.json1
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"