diff options
author | crupest <crupest@outlook.com> | 2019-04-11 20:11:23 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2019-04-11 20:11:23 +0800 |
commit | f562660f52ce055e243b937a988f04c90ad3ae55 (patch) | |
tree | 49dc35791778a4ed1403319708046ac8823210b6 | |
parent | 1eb6d9abfc24eec380b7b5d7423102a53041239e (diff) | |
download | timeline-f562660f52ce055e243b937a988f04c90ad3ae55.tar.gz timeline-f562660f52ce055e243b937a988f04c90ad3ae55.tar.bz2 timeline-f562660f52ce055e243b937a988f04c90ad3ae55.zip |
Change create token api.
11 files changed, 116 insertions, 114 deletions
diff --git a/Timeline.Tests/AuthorizationUnitTest.cs b/Timeline.Tests/AuthorizationUnitTest.cs index 1566f2ac..2693366c 100644 --- a/Timeline.Tests/AuthorizationUnitTest.cs +++ b/Timeline.Tests/AuthorizationUnitTest.cs @@ -1,10 +1,6 @@ using Microsoft.AspNetCore.Mvc.Testing; -using Newtonsoft.Json; -using System; using System.Net; -using System.Net.Http; using System.Threading.Tasks; -using Timeline.Controllers; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; using Xunit; diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs index a4cb8c65..ccb2a372 100644 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs @@ -1,11 +1,9 @@ using Newtonsoft.Json; using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Timeline.Controllers; +using Timeline.Entities; using Xunit; namespace Timeline.Tests.Helpers.Authentication @@ -14,13 +12,16 @@ namespace Timeline.Tests.Helpers.Authentication { private const string CreateTokenUrl = "/api/User/CreateToken"; - public static async Task<UserController.CreateTokenResult> CreateUserTokenAsync(this HttpClient client, string username, string password) + public static async Task<CreateTokenResponse> CreateUserTokenAsync(this HttpClient client, string username, string password, bool assertSuccess = true) { - var response = await client.PostAsJsonAsync(CreateTokenUrl, new UserController.UserCredentials { Username = username, Password = password }); + var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = JsonConvert.DeserializeObject<UserController.CreateTokenResult>(await response.Content.ReadAsStringAsync()); + var result = JsonConvert.DeserializeObject<CreateTokenResponse>(await response.Content.ReadAsStringAsync()); + + if (assertSuccess) + Assert.True(result.Success); return result; } diff --git a/Timeline.Tests/JwtTokenUnitTest.cs b/Timeline.Tests/JwtTokenUnitTest.cs index e55bc82c..7e881895 100644 --- a/Timeline.Tests/JwtTokenUnitTest.cs +++ b/Timeline.Tests/JwtTokenUnitTest.cs @@ -1,14 +1,8 @@ using Microsoft.AspNetCore.Mvc.Testing; using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Timeline.Controllers; -using Timeline.Services; +using Timeline.Entities; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; using Xunit; @@ -18,6 +12,7 @@ namespace Timeline.Tests { public class JwtTokenUnitTest : IClassFixture<WebApplicationFactory<Startup>> { + private const string CreateTokenUrl = "/api/User/CreateToken"; private const string ValidateTokenUrl = "/api/User/ValidateToken"; private readonly WebApplicationFactory<Startup> _factory; @@ -28,53 +23,59 @@ namespace Timeline.Tests } [Fact] - public async void ValidateToken_BadTokenTest() + public async void CreateTokenTest_BadCredential() { using (var client = _factory.CreateDefaultClient()) { - var response = await client.PostAsync(ValidateTokenUrl, new StringContent("bad token hahaha", Encoding.UTF8, "text/plain")); - + var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "???", Password = "???" }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var validationInfo = JsonConvert.DeserializeObject<TokenValidationResult>(await response.Content.ReadAsStringAsync()); - - Assert.False(validationInfo.IsValid); - Assert.Null(validationInfo.UserInfo); + var result = JsonConvert.DeserializeObject<CreateTokenResponse>(await response.Content.ReadAsStringAsync()); + Assert.False(result.Success); + Assert.Null(result.Token); + Assert.Null(result.UserInfo); } } [Fact] - public async void ValidateToken_PlainTextGoodTokenTest() + public async void CreateTokenTest_GoodCredential() { using (var client = _factory.CreateDefaultClient()) { - var createTokenResult = await client.CreateUserTokenAsync("admin", "admin"); + var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "user", Password = "user" }); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = JsonConvert.DeserializeObject<CreateTokenResponse>(await response.Content.ReadAsStringAsync()); + Assert.True(result.Success); + Assert.NotNull(result.Token); + Assert.NotNull(result.UserInfo); + } + } - var response = await client.PostAsync(ValidateTokenUrl, new StringContent(createTokenResult.Token, Encoding.UTF8, "text/plain")); + [Fact] + public async void ValidateTokenTest_BadToken() + { + using (var client = _factory.CreateDefaultClient()) + { + var response = await client.PostAsJsonAsync(ValidateTokenUrl, new TokenValidationRequest { Token = "bad token hahaha" }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = JsonConvert.DeserializeObject<TokenValidationResult>(await response.Content.ReadAsStringAsync()); + var validationInfo = JsonConvert.DeserializeObject<TokenValidationResponse>(await response.Content.ReadAsStringAsync()); - Assert.True(result.IsValid); - Assert.NotNull(result.UserInfo); - Assert.Equal(createTokenResult.UserInfo.Username, result.UserInfo.Username); - Assert.Equal(createTokenResult.UserInfo.Roles, result.UserInfo.Roles); + Assert.False(validationInfo.IsValid); + Assert.Null(validationInfo.UserInfo); } } [Fact] - public async void ValidateToken_JsonGoodTokenTest() + public async void ValidateTokenTest_GoodToken() { using (var client = _factory.CreateDefaultClient()) { var createTokenResult = await client.CreateUserTokenAsync("admin", "admin"); - var response = await client.PostAsJsonAsync(ValidateTokenUrl, new UserController.TokenValidationRequest { Token = createTokenResult.Token }); - + var response = await client.PostAsJsonAsync(ValidateTokenUrl, new TokenValidationRequest { Token = createTokenResult.Token }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var result = JsonConvert.DeserializeObject<TokenValidationResult>(await response.Content.ReadAsStringAsync()); + var result = JsonConvert.DeserializeObject<TokenValidationResponse>(await response.Content.ReadAsStringAsync()); Assert.True(result.IsValid); Assert.NotNull(result.UserInfo); diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts b/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts index 22e44dd6..3358a9d9 100644 --- a/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts +++ b/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts @@ -1,25 +1,29 @@ -export abstract class LoginError extends Error { } - -export class BadNetworkError extends LoginError { +export class BadNetworkError extends Error { constructor() { super('Network is bad.'); } } -export class AlreadyLoginError extends LoginError { +export class AlreadyLoginError extends Error { constructor() { super('Internal logical error. There is already a token saved. Please call validateUserLoginState first.'); } } -export class BadCredentialsError extends LoginError { +export class BadCredentialsError extends Error { constructor() { super('Username or password is wrong.'); } } -export class UnknownError extends LoginError { +export class UnknownError extends Error { constructor(public internalError?: any) { super('Sorry, unknown error occured!'); } } + +export class ServerInternalError extends Error { + constructor(message?: string) { + super('Wrong server response. ' + message); + } +} diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts b/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts index 5664cf7c..f52233c9 100644 --- a/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts +++ b/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts @@ -6,8 +6,9 @@ export const validateTokenUrl = '/api/User/ValidateToken'; export type CreateTokenRequest = UserCredentials; export interface CreateTokenResponse { - token: string; - userInfo: UserInfo; + success: boolean; + token?: string; + userInfo?: UserInfo; } export interface ValidateTokenRequest { diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts index 6906ed60..15755382 100644 --- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts +++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts @@ -102,6 +102,7 @@ describe('InternalUserService', () => { request.url === createTokenUrl && request.body !== null && request.body.username === mockUserCredentials.username && request.body.password === mockUserCredentials.password).flush(<CreateTokenResponse>{ + success: true, token: mockToken, userInfo: mockUserInfo }); diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts index 6de355f2..66eafde9 100644 --- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts +++ b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts @@ -5,7 +5,7 @@ import { Router } from '@angular/router'; import { Observable, throwError, BehaviorSubject, of } from 'rxjs'; import { map, catchError, retry, switchMap, tap, filter } from 'rxjs/operators'; -import { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError } from './errors'; +import { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError, ServerInternalError } from './errors'; import { createTokenUrl, validateTokenUrl, CreateTokenRequest, CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse @@ -84,7 +84,7 @@ export class InternalUserService { if (userInfo) { return of(userInfo); } else { - return throwError(new Error('Wrong server response. IsValid is true but UserInfo is null.')); + return throwError(new ServerInternalError('IsValid is true but UserInfo is null.')); } } else { return of(null); @@ -117,21 +117,28 @@ export class InternalUserService { if (error.error instanceof ErrorEvent) { console.error('An error occurred when login: ' + error.error.message); return throwError(new BadNetworkError()); - } else if (error.status === 400) { - console.error('An error occurred when login: wrong credentials.'); - return throwError(new BadCredentialsError()); } else { console.error('An unknown error occurred when login: ' + error); return throwError(new UnknownError(error)); } }), - map(result => { - this.token = result.token; - if (info.rememberMe) { - this.window.localStorage.setItem(TOKEN_STORAGE_KEY, result.token); + switchMap(result => { + if (result.success) { + if (result.token && result.userInfo) { + this.token = result.token; + if (info.rememberMe) { + this.window.localStorage.setItem(TOKEN_STORAGE_KEY, result.token); + } + this.userInfoSubject.next(result.userInfo); + return of(result.userInfo); + } else { + console.error('An error occurred when login: server return wrong data.'); + return throwError(new ServerInternalError('Token or userInfo is null.')); + } + } else { + console.error('An error occurred when login: wrong credentials.'); + return throwError(new BadCredentialsError()); } - this.userInfoSubject.next(result.userInfo); - return result.userInfo; }) ); } diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 45242ce3..eb1b8513 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -15,23 +15,6 @@ namespace Timeline.Controllers public const int LogInFailed = 4001; } - public class UserCredentials - { - public string Username { get; set; } - public string Password { get; set; } - } - - public class CreateTokenResult - { - public string Token { get; set; } - public UserInfo UserInfo { get; set; } - } - - public class TokenValidationRequest - { - public string Token { get; set; } - } - private readonly IUserService _userService; private readonly IJwtService _jwtService; private readonly ILogger<UserController> _logger; @@ -45,39 +28,31 @@ namespace Timeline.Controllers [HttpPost("[action]")] [AllowAnonymous] - public ActionResult<CreateTokenResult> CreateToken([FromBody] UserCredentials credentials) + public ActionResult<CreateTokenResponse> CreateToken([FromBody] CreateTokenRequest request) { - var user = _userService.Authenticate(credentials.Username, credentials.Password); + var user = _userService.Authenticate(request.Username, request.Password); if (user == null) { - _logger.LogInformation(LoggingEventIds.LogInFailed, "Attemp to login with username: {} and password: {} failed.", credentials.Username, credentials.Password); - return BadRequest(); + _logger.LogInformation(LoggingEventIds.LogInFailed, "Attemp to login with username: {} and password: {} failed.", request.Username, request.Password); + return Ok(new CreateTokenResponse + { + Success = false + }); } - _logger.LogInformation(LoggingEventIds.LogInSucceeded, "Login with username: {} succeeded.", credentials.Username); + _logger.LogInformation(LoggingEventIds.LogInSucceeded, "Login with username: {} succeeded.", request.Username); - var result = new CreateTokenResult + return Ok(new CreateTokenResponse { + Success = true, Token = _jwtService.GenerateJwtToken(user), 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) + public ActionResult<TokenValidationResponse> ValidateToken([FromBody] TokenValidationRequest request) { var result = _jwtService.ValidateJwtToken(request.Token); return Ok(result); diff --git a/Timeline/Entities/Token.cs b/Timeline/Entities/Token.cs new file mode 100644 index 00000000..ce5b92ff --- /dev/null +++ b/Timeline/Entities/Token.cs @@ -0,0 +1,26 @@ +namespace Timeline.Entities +{ + public class CreateTokenRequest + { + public string Username { get; set; } + public string Password { get; set; } + } + + public class CreateTokenResponse + { + public bool Success { get; set; } + public string Token { get; set; } + public UserInfo UserInfo { get; set; } + } + + public class TokenValidationRequest + { + public string Token { get; set; } + } + + public class TokenValidationResponse + { + public bool IsValid { get; set; } + public UserInfo UserInfo { get; set; } + } +} diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs index a01f3f2b..abdde908 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -11,12 +11,6 @@ using Timeline.Entities; namespace Timeline.Services { - public class TokenValidationResult - { - public bool IsValid { get; set; } - public UserInfo UserInfo { get; set; } - } - public interface IJwtService { /// <summary> @@ -30,17 +24,17 @@ namespace Timeline.Services /// <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 + /// If token is invalid, return a <see cref="TokenValidationResponse"/> with + /// <see cref="TokenValidationResponse.IsValid"/> set to false and + /// <see cref="TokenValidationResponse.UserInfo"/> set to null. + /// If token is valid, return a <see cref="TokenValidationResponse"/> with + /// <see cref="TokenValidationResponse.IsValid"/> set to true and + /// <see cref="TokenValidationResponse.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); + TokenValidationResponse ValidateJwtToken(string token); } @@ -86,7 +80,7 @@ namespace Timeline.Services } - public TokenValidationResult ValidateJwtToken(string token) + public TokenValidationResponse ValidateJwtToken(string token) { if (token == null) return null; @@ -114,7 +108,7 @@ namespace Timeline.Services Roles = identity.FindAll(identity.RoleClaimType).Select(claim => claim.Value).ToArray() }; - return new TokenValidationResult + return new TokenValidationResponse { IsValid = true, UserInfo = userInfo @@ -123,7 +117,7 @@ namespace Timeline.Services catch (Exception e) { _logger.LogInformation(e, "Token validation failed! Token is {} .", token); - return new TokenValidationResult { IsValid = false }; + return new TokenValidationResponse { IsValid = false }; } } } diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 330d2981..e55eb90d 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -27,10 +27,6 @@ <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" /> </ItemGroup> - <ItemGroup> - <Folder Include="bin\" /> - </ItemGroup> - <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') "> <!-- Ensure Node.js is installed --> <Exec Command="node --version" ContinueOnError="true"> |