diff options
Diffstat (limited to 'BackEnd/Timeline')
9 files changed, 186 insertions, 15 deletions
diff --git a/BackEnd/Timeline/Controllers/TokenController.cs b/BackEnd/Timeline/Controllers/TokenController.cs index 9ee5a09f..7fba0bc5 100644 --- a/BackEnd/Timeline/Controllers/TokenController.cs +++ b/BackEnd/Timeline/Controllers/TokenController.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
@@ -77,7 +77,7 @@ namespace Timeline.Controllers [AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- public async Task<ActionResult<HttpVerifyTokenResponse>> Verify([FromBody] HttpVerifyTokenRequest request)
+ public async Task<ActionResult<HttpVerifyTokenResponse>> Verify([FromBody] HttpVerifyOrRevokeTokenRequest request)
{
try
{
diff --git a/BackEnd/Timeline/Controllers/V2/TokenV2Controller.cs b/BackEnd/Timeline/Controllers/V2/TokenV2Controller.cs new file mode 100644 index 00000000..b129758a --- /dev/null +++ b/BackEnd/Timeline/Controllers/V2/TokenV2Controller.cs @@ -0,0 +1,132 @@ +using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Services.Token; +using Timeline.Services.User; + +namespace Timeline.Controllers.V2 +{ + [ApiController] + [Route("v2/token")] + public class TokenV2Controller : V2ControllerBase + { + private readonly IUserService _userService; + private readonly IUserTokenService _userTokenService; + private readonly IClock _clock; + + public TokenV2Controller(IUserService userService, IUserTokenService userTokenService, IClock clock) + { + _userService = userService; + _userTokenService = userTokenService; + _clock = clock; + } + + private const string BadCredentialMessage = "Username or password is wrong."; + + /// <summary> + /// Create a new token for a user. + /// </summary> + /// <returns>Result of token creation.</returns> + [HttpPost("create")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task<ActionResult<HttpCreateTokenResponse>> CreateAsync([FromBody] HttpCreateTokenRequestV2 request) + { + + try + { + DateTime? expireTime = null; + if (request.ValidDays is not null) + expireTime = _clock.GetCurrentTime().AddDays(request.ValidDays.Value); + + var userId = await _userService.VerifyCredential(request.Username, request.Password); + var token = await _userTokenService.CreateTokenAsync(userId, expireTime); + var user = await _userService.GetUserAsync(userId); + + return new HttpCreateTokenResponse + { + Token = token, + User = await MapAsync<HttpUser>(user) + }; + } + catch (EntityNotExistException) + { + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, BadCredentialMessage)); + } + catch (BadPasswordException) + { + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, BadCredentialMessage)); + } + } + + private const string TokenExpiredMessage = "The token has expired."; + private const string TokenInvalidMessage = "The token is invalid."; + + /// <summary> + /// Verify a token. + /// </summary> + /// <returns>Result of token verification.</returns> + [HttpPost("verify")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task<ActionResult<HttpVerifyTokenResponseV2>> VerifyAsync([FromBody] HttpVerifyOrRevokeTokenRequest request) + { + try + { + var tokenInfo = await _userTokenService.ValidateTokenAsync(request.Token); + var user = await _userService.GetUserAsync(tokenInfo.UserId); + return new HttpVerifyTokenResponseV2 + { + User = await MapAsync<HttpUser>(user), + ExpireAt = tokenInfo.ExpireAt + }; + } + catch (UserTokenExpiredException) + { + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, TokenExpiredMessage)); + } + catch (UserTokenException) + { + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, TokenInvalidMessage)); + } + } + + [HttpPost("revoke")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [Authorize] + public async Task<ActionResult> RevokeAsync([FromBody] HttpVerifyOrRevokeTokenRequest body) + { + UserTokenInfo userTokenInfo; + try + { + userTokenInfo = await _userTokenService.ValidateTokenAsync(body.Token, false); + } + catch (UserTokenException) + { + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, TokenInvalidMessage)); + } + + if (userTokenInfo.UserId != GetAuthUserId()) + return UnprocessableEntity(new ErrorResponse(ErrorResponse.InvalidRequest, TokenInvalidMessage)); + + await _userTokenService.RevokeTokenAsync(body.Token); + + return NoContent(); + } + + [HttpPost("revokeall")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [Authorize] + public async Task<ActionResult> RevokeAllAsync() + { + await _userTokenService.RevokeAllTokenByUserIdAsync(GetAuthUserId()); + return NoContent(); + } + } +} + diff --git a/BackEnd/Timeline/Models/Http/HttpCreateTokenRequest.cs b/BackEnd/Timeline/Models/Http/HttpCreateTokenRequest.cs index 2a20d490..5881447a 100644 --- a/BackEnd/Timeline/Models/Http/HttpCreateTokenRequest.cs +++ b/BackEnd/Timeline/Models/Http/HttpCreateTokenRequest.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations;
using Timeline.Controllers;
namespace Timeline.Models.Http
diff --git a/BackEnd/Timeline/Models/Http/HttpCreateTokenRequestV2.cs b/BackEnd/Timeline/Models/Http/HttpCreateTokenRequestV2.cs new file mode 100644 index 00000000..acd8d2e5 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/HttpCreateTokenRequestV2.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Timeline.Models.Http +{ + public class HttpCreateTokenRequestV2 + { + /// <summary> + /// The username. + /// </summary> + [Required] + public string Username { get; set; } = default!; + /// <summary> + /// The password. + /// </summary> + [Required] + public string Password { get; set; } = default!; + /// <summary> + /// Optional token validation period. In days. If not specified, the token will be valid until being revoked explicited. + /// </summary> + [Range(1, 365)] + public int? ValidDays { get; set; } + } +} + diff --git a/BackEnd/Timeline/Models/Http/HttpVerifyTokenRequest.cs b/BackEnd/Timeline/Models/Http/HttpVerifyTokenRequest.cs index 98f86455..a0cca2e9 100644 --- a/BackEnd/Timeline/Models/Http/HttpVerifyTokenRequest.cs +++ b/BackEnd/Timeline/Models/Http/HttpVerifyTokenRequest.cs @@ -1,11 +1,8 @@ -using Timeline.Controllers;
+using Timeline.Controllers;
namespace Timeline.Models.Http
{
- /// <summary>
- /// Request model for <see cref="TokenController.Verify(HttpVerifyTokenRequest)"/>.
- /// </summary>
- public class HttpVerifyTokenRequest
+ public class HttpVerifyOrRevokeTokenRequest
{
/// <summary>
/// The token to verify.
diff --git a/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponse.cs b/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponse.cs index ae8eb018..35789081 100644 --- a/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponse.cs +++ b/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponse.cs @@ -1,10 +1,10 @@ -using Timeline.Controllers;
+using Timeline.Controllers;
namespace Timeline.Models.Http
{
/// <summary>
- /// Response model for <see cref="TokenController.Verify(HttpVerifyTokenRequest)"/>.
+ /// Response model for <see cref="TokenController.Verify(HttpVerifyOrRevokeTokenRequest)"/>.
/// </summary>
public class HttpVerifyTokenResponse
{
diff --git a/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponseV2.cs b/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponseV2.cs new file mode 100644 index 00000000..c91771cf --- /dev/null +++ b/BackEnd/Timeline/Models/Http/HttpVerifyTokenResponseV2.cs @@ -0,0 +1,16 @@ +
+using System; + +namespace Timeline.Models.Http
+{
+
+ public class HttpVerifyTokenResponseV2
+ {
+ /// <summary>
+ /// The user owning the token.
+ /// </summary>
+ public HttpUser User { get; set; } = default!;
+
+ public DateTime? ExpireAt { get; set; }
+ }
+}
diff --git a/BackEnd/Timeline/Services/Token/IUserTokenService.cs b/BackEnd/Timeline/Services/Token/IUserTokenService.cs index 22fb0fb4..a9689f57 100644 --- a/BackEnd/Timeline/Services/Token/IUserTokenService.cs +++ b/BackEnd/Timeline/Services/Token/IUserTokenService.cs @@ -17,11 +17,12 @@ namespace Timeline.Services.Token /// Verify a token and get the info of the token.
/// </summary>
/// <param name="token">The token to verify.</param>
+ /// <param name="checkLifetime">Whether to check lifetime of token.</param>
/// <returns>The info of the token.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
/// <exception cref="UserTokenException">Thrown when the token is not valid for reasons other than expired.</exception>
- /// <exception cref="UserTokenExpiredException">Thrown when the token is expired.</exception>
- Task<UserTokenInfo> ValidateTokenAsync(string token);
+ /// <exception cref="UserTokenExpiredException">Thrown when <paramref name="checkLifetime"/> is true and the token is expired.</exception>
+ Task<UserTokenInfo> ValidateTokenAsync(string token, bool checkLifetime = true);
/// <summary>
/// Revoke a token to make it no longer valid.
diff --git a/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs b/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs index 4d79295a..ceef4798 100644 --- a/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs +++ b/BackEnd/Timeline/Services/Token/SecureRandomUserTokenService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; @@ -81,7 +81,7 @@ namespace Timeline.Services.Token } /// <inheritdoc/> - public async Task<UserTokenInfo> ValidateTokenAsync(string token) + public async Task<UserTokenInfo> ValidateTokenAsync(string token, bool checkLifetime = true) { var entity = await _databaseContext.UserTokens.Where(t => t.Token == token && !t.Deleted).SingleOrDefaultAsync(); @@ -92,7 +92,7 @@ namespace Timeline.Services.Token var currentTime = _clock.GetCurrentTime(); - if (entity.ExpireAt.HasValue && entity.ExpireAt.Value <= currentTime) + if (checkLifetime && entity.ExpireAt.HasValue && entity.ExpireAt.Value <= currentTime) { throw new UserTokenExpiredException(token, entity.ExpireAt.Value, currentTime); } |