aboutsummaryrefslogtreecommitdiff
path: root/Timeline
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2019-02-14 23:05:04 +0800
committercrupest <crupest@outlook.com>2019-02-14 23:05:04 +0800
commit3c140656ebe6ed34dda9356a01dbff205651e641 (patch)
tree8b8ca7331c9510b897042737a5cbbc0f77b1b736 /Timeline
parentde90f0413553a23f8ebba1343c6e96c63e0c9748 (diff)
downloadtimeline-3c140656ebe6ed34dda9356a01dbff205651e641.tar.gz
timeline-3c140656ebe6ed34dda9356a01dbff205651e641.tar.bz2
timeline-3c140656ebe6ed34dda9356a01dbff205651e641.zip
Develop user token interface.
Diffstat (limited to 'Timeline')
-rw-r--r--Timeline/Controllers/UserController.cs36
-rw-r--r--Timeline/Controllers/UserTestController.cs (renamed from Timeline/Controllers/TestController.cs)16
-rw-r--r--Timeline/Entities/User.cs15
-rw-r--r--Timeline/Formatters/StringInputFormatter.cs32
-rw-r--r--Timeline/Services/JwtService.cs74
-rw-r--r--Timeline/Startup.cs9
6 files changed, 164 insertions, 18 deletions
diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs
index 9d6970e7..1ffed22b 100644
--- a/Timeline/Controllers/UserController.cs
+++ b/Timeline/Controllers/UserController.cs
@@ -1,6 +1,9 @@
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
+using System.IO;
+using Timeline.Entities;
using Timeline.Services;
namespace Timeline.Controllers
@@ -20,10 +23,15 @@ namespace Timeline.Controllers
public string Password { get; set; }
}
- public class LoginInfo
+ public class CreateTokenResult
+ {
+ public string Token { get; set; }
+ public UserInfo UserInfo { get; set; }
+ }
+
+ public class TokenValidationRequest
{
public string Token { get; set; }
- public string[] Roles { get; set; }
}
private readonly IUserService _userService;
@@ -39,7 +47,7 @@ namespace Timeline.Controllers
[HttpPost("[action]")]
[AllowAnonymous]
- public ActionResult<LoginInfo> LogIn([FromBody] UserCredentials credentials)
+ public ActionResult<CreateTokenResult> CreateToken([FromBody] UserCredentials credentials)
{
var user = _userService.Authenticate(credentials.Username, credentials.Password);
@@ -50,13 +58,31 @@ namespace Timeline.Controllers
_logger.LogInformation(LoggingEventIds.LogInSucceeded, "Login with username: {} succeeded.", credentials.Username);
- var result = new LoginInfo
+ var result = new CreateTokenResult
{
Token = _jwtService.GenerateJwtToken(user),
- Roles = user.Roles
+ UserInfo = user.GetUserInfo()
};
return Ok(result);
}
+
+ [HttpPost("[action]")]
+ [Consumes("text/plain")]
+ [AllowAnonymous]
+ public ActionResult<TokenValidationResult> ValidateToken([FromBody] string token)
+ {
+ var result = _jwtService.ValidateJwtToken(token);
+ return Ok(result);
+ }
+
+ [HttpPost("[action]")]
+ [Consumes("application/json")]
+ [AllowAnonymous]
+ public ActionResult<TokenValidationResult> ValidateToken([FromBody] TokenValidationRequest request)
+ {
+ var result = _jwtService.ValidateJwtToken(request.Token);
+ return Ok(result);
+ }
}
}
diff --git a/Timeline/Controllers/TestController.cs b/Timeline/Controllers/UserTestController.cs
index 1563830c..7fb6850b 100644
--- a/Timeline/Controllers/TestController.cs
+++ b/Timeline/Controllers/UserTestController.cs
@@ -7,28 +7,28 @@ using Microsoft.AspNetCore.Mvc;
namespace Timeline.Controllers
{
- [Route("api/[controller]")]
- public class TestController : Controller
+ [Route("api/test/User")]
+ public class UserTestController : Controller
{
[HttpGet("[action]")]
[Authorize]
- public string Action1()
+ public ActionResult NeedAuthorize()
{
- return "test";
+ return Ok();
}
[HttpGet("[action]")]
[Authorize(Roles = "User,Admin")]
- public string Action2()
+ public ActionResult BothUserAndAdmin()
{
- return "test";
+ return Ok();
}
[HttpGet("[action]")]
[Authorize(Roles = "Admin")]
- public string Action3()
+ public ActionResult OnlyAdmin()
{
- return "test";
+ return Ok();
}
}
}
diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/User.cs
index 3f7e9ac6..50463b57 100644
--- a/Timeline/Entities/User.cs
+++ b/Timeline/Entities/User.cs
@@ -11,5 +11,20 @@ namespace Timeline.Entities
public string Username { get; set; }
public string Password { get; set; }
public string[] Roles { get; set; }
+
+ public UserInfo GetUserInfo()
+ {
+ return new UserInfo
+ {
+ Username = this.Username,
+ Roles = this.Roles
+ };
+ }
+ }
+
+ public class UserInfo
+ {
+ public string Username { get; set; }
+ public string[] Roles { get; set; }
}
}
diff --git a/Timeline/Formatters/StringInputFormatter.cs b/Timeline/Formatters/StringInputFormatter.cs
new file mode 100644
index 00000000..f2475137
--- /dev/null
+++ b/Timeline/Formatters/StringInputFormatter.cs
@@ -0,0 +1,32 @@
+using Microsoft.AspNetCore.Mvc.Formatters;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Timeline.Formatters
+{
+ public class StringInputFormatter : TextInputFormatter
+ {
+ public StringInputFormatter()
+ {
+ SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/plain"));
+
+ SupportedEncodings.Add(Encoding.UTF8);
+ SupportedEncodings.Add(Encoding.Unicode);
+ }
+
+ public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding)
+ {
+ var request = context.HttpContext.Request;
+ using (var reader = new StreamReader(request.Body, effectiveEncoding))
+ {
+ var stringContent = reader.ReadToEnd();
+ return InputFormatterResult.SuccessAsync(stringContent);
+ }
+ }
+ }
+}
diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs
index 1b465dd9..a01f3f2b 100644
--- a/Timeline/Services/JwtService.cs
+++ b/Timeline/Services/JwtService.cs
@@ -1,17 +1,22 @@
-using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Logging;
+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 class TokenValidationResult
+ {
+ public bool IsValid { get; set; }
+ public UserInfo UserInfo { get; set; }
+ }
+
public interface IJwtService
{
/// <summary>
@@ -21,16 +26,34 @@ namespace Timeline.Services
/// <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);
+
+ /// <summary>
+ /// Validate a JWT token.
+ /// Return null is <paramref name="token"/> is null.
+ /// If token is invalid, return a <see cref="TokenValidationResult"/> with
+ /// <see cref="TokenValidationResult.IsValid"/> set to false and
+ /// <see cref="TokenValidationResult.UserInfo"/> set to null.
+ /// If token is valid, return a <see cref="TokenValidationResult"/> with
+ /// <see cref="TokenValidationResult.IsValid"/> set to true and
+ /// <see cref="TokenValidationResult.UserInfo"/> filled with the user info
+ /// in the token.
+ /// </summary>
+ /// <param name="token">The token string to validate.</param>
+ /// <returns>Null if <paramref name="token"/> is null. Or the result.</returns>
+ TokenValidationResult ValidateJwtToken(string token);
+
}
public class JwtService : IJwtService
{
private readonly IOptionsMonitor<JwtConfig> _jwtConfig;
private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
+ private readonly ILogger<JwtService> _logger;
- public JwtService(IOptionsMonitor<JwtConfig> jwtConfig)
+ public JwtService(IOptionsMonitor<JwtConfig> jwtConfig, ILogger<JwtService> logger)
{
_jwtConfig = jwtConfig;
+ _logger = logger;
}
public string GenerateJwtToken(User user)
@@ -42,6 +65,7 @@ namespace Timeline.Services
var identity = new ClaimsIdentity();
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
+ identity.AddClaim(new Claim(identity.NameClaimType, user.Username));
identity.AddClaims(user.Roles.Select(role => new Claim(identity.RoleClaimType, role)));
var tokenDescriptor = new SecurityTokenDescriptor()
@@ -60,5 +84,47 @@ namespace Timeline.Services
return tokenString;
}
+
+
+ public TokenValidationResult ValidateJwtToken(string token)
+ {
+ if (token == null)
+ return null;
+
+ var config = _jwtConfig.CurrentValue;
+
+ try
+ {
+ var principal = _tokenHandler.ValidateToken(token, new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateIssuerSigningKey = true,
+ ValidateLifetime = true,
+ ValidIssuer = config.Issuer,
+ ValidAudience = config.Audience,
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey))
+ }, out SecurityToken validatedToken);
+
+ var identity = principal.Identity as ClaimsIdentity;
+
+ var userInfo = new UserInfo
+ {
+ Username = identity.FindAll(identity.NameClaimType).Select(claim => claim.Value).Single(),
+ Roles = identity.FindAll(identity.RoleClaimType).Select(claim => claim.Value).ToArray()
+ };
+
+ return new TokenValidationResult
+ {
+ IsValid = true,
+ UserInfo = userInfo
+ };
+ }
+ catch (Exception e)
+ {
+ _logger.LogInformation(e, "Token validation failed! Token is {} .", token);
+ return new TokenValidationResult { IsValid = false };
+ }
+ }
}
}
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs
index 2a0f437f..6381a58a 100644
--- a/Timeline/Startup.cs
+++ b/Timeline/Startup.cs
@@ -10,6 +10,10 @@ using System.Text;
using Timeline.Configs;
using Timeline.Services;
using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.AspNetCore.Mvc.Formatters;
+using System;
+using System.Threading.Tasks;
+using Timeline.Formatters;
namespace Timeline
{
@@ -25,7 +29,10 @@ namespace Timeline
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
- services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
+ services.AddMvc(options =>
+ {
+ options.InputFormatters.Add(new StringInputFormatter());
+ }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>