From 0346660adc6d12e6d7b7a9cb45c32f08f49b9a04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Jul 2022 14:40:31 +0000 Subject: [PATCH 1/5] Bump AuthEndpoints.Core from 1.4.3 to 1.4.4 Bumps [AuthEndpoints.Core](https://github.com/madeyoga/AuthEndpoints) from 1.4.3 to 1.4.4. - [Release notes](https://github.com/madeyoga/AuthEndpoints/releases) - [Commits](https://github.com/madeyoga/AuthEndpoints/commits) --- updated-dependencies: - dependency-name: AuthEndpoints.Core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/AuthEndpoints/AuthEndpoints.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AuthEndpoints/AuthEndpoints.csproj b/src/AuthEndpoints/AuthEndpoints.csproj index 27afd7a..57e289d 100644 --- a/src/AuthEndpoints/AuthEndpoints.csproj +++ b/src/AuthEndpoints/AuthEndpoints.csproj @@ -32,7 +32,7 @@ - + From 019f3c48e2bca2cc22aec6b59b1c120d1d71a94f Mon Sep 17 00:00:00 2001 From: MadeYoga Date: Tue, 26 Jul 2022 16:51:07 +0800 Subject: [PATCH 2/5] Add more tests for minimal api --- tests/AuthEndpoints.MinimalApi.Tests/Test1.cs | 106 ++++++++++++++++-- 1 file changed, 98 insertions(+), 8 deletions(-) diff --git a/tests/AuthEndpoints.MinimalApi.Tests/Test1.cs b/tests/AuthEndpoints.MinimalApi.Tests/Test1.cs index f2f3414..74e5258 100644 --- a/tests/AuthEndpoints.MinimalApi.Tests/Test1.cs +++ b/tests/AuthEndpoints.MinimalApi.Tests/Test1.cs @@ -8,7 +8,7 @@ namespace AuthEndpoints.MinimalApi.Tests; public class Test1 { [Fact] - public async Task Can_RegisterUser() + public async Task CreateUser() { await using var application = new AuthApplication(); var client = application.CreateClient(); @@ -23,7 +23,37 @@ public async Task Can_RegisterUser() } [Fact] - public async Task Cannot_RegisterDuplicateUsername() + public async Task CreateUser_InvalidEmail_Conflict() + { + await using var application = new AuthApplication(); + var client = application.CreateClient(); + var response = await client.PostAsJsonAsync("/users", new RegisterRequest + { + Email = "invalidEmail", + Username = "invalidEmail", + Password = "testtest", + ConfirmPassword = "testtest", + }); + Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); + } + + [Fact] + public async Task CreateUser_ConfirmPasswordNotMatch_BadRequest() + { + await using var application = new AuthApplication(); + var client = application.CreateClient(); + var response = await client.PostAsJsonAsync("/users", new RegisterRequest + { + Email = "passwordNotMatch@test.com", + Username = "passwordNotMatch", + Password = "test1", + ConfirmPassword = "test2", + }); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task CreateUser_DuplicateUsername_Conflict() { await using var application = new AuthApplication(); var client = application.CreateClient(); @@ -46,7 +76,7 @@ public async Task Cannot_RegisterDuplicateUsername() } [Fact] - public async Task Cannot_RegisterDuplicateEmail() + public async Task CreateUser_DuplicateEmail_Conflict() { await using var application = new AuthApplication(); var client = application.CreateClient(); @@ -69,7 +99,7 @@ public async Task Cannot_RegisterDuplicateEmail() } [Fact] - public async Task Can_CreateJwt() + public async Task CreateJwt() { await using var application = new AuthApplication(); var client = application.CreateClient(); @@ -91,7 +121,36 @@ public async Task Can_CreateJwt() } [Fact] - public async Task GetLoggedInUserData() + public async Task CreateJwt_InvalidCredentials_Unauthorized() + { + await using var application = new AuthApplication(); + var client = application.CreateClient(); + var registerResp = await client.PostAsJsonAsync("/users", new RegisterRequest + { + Email = "CreateJwt_InvalidCredentials@test.id", + Username = "InvalidCredentials", + Password = "testtest", + ConfirmPassword = "testtest", + }); + Assert.Equal(HttpStatusCode.OK, registerResp.StatusCode); + + var response = await client.PostAsJsonAsync("/jwt/create", new LoginRequest + { + Username = "InvalidCredentials1", + Password = "wrongpassword" + }); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + var response2 = await client.PostAsJsonAsync("/jwt/create", new LoginRequest + { + Username = "wrongusername", + Password = "testtest" + }); + Assert.Equal(HttpStatusCode.Unauthorized, response2.StatusCode); + } + + [Fact] + public async Task GetLoggedInUserData_Authorized() { await using var application = new AuthApplication(); var client = application.CreateClient(); @@ -120,7 +179,16 @@ public async Task GetLoggedInUserData() } [Fact] - public async Task Can_RefreshJwt() + public async Task GetLoggedInUserData_Unauthorized() + { + await using var application = new AuthApplication(); + var client = application.CreateClient(); + var response2 = await client.GetAsync("/users/me"); + Assert.Equal(HttpStatusCode.Unauthorized, response2.StatusCode); + } + + [Fact] + public async Task RefreshJwt() { await using var application = new AuthApplication(); var client = application.CreateClient(); @@ -143,7 +211,6 @@ public async Task Can_RefreshJwt() var result = await response1.Content.ReadFromJsonAsync(); Assert.NotNull(result); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result!.AccessToken!); var response2 = await client.PostAsJsonAsync("/jwt/refresh", new RefreshRequest { RefreshToken = result!.RefreshToken!, @@ -152,7 +219,20 @@ public async Task Can_RefreshJwt() } [Fact] - public async Task Can_VerifyJwt() + public async Task RefreshJwt_InvalidToken_BadRequest() + { + await using var application = new AuthApplication(); + var client = application.CreateClient(); + + var response = await client.PostAsJsonAsync("/jwt/refresh", new RefreshRequest + { + RefreshToken = "RandomToken", + }); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task VerifyJwt() { await using var application = new AuthApplication(); var client = application.CreateClient(); @@ -179,4 +259,14 @@ public async Task Can_VerifyJwt() var response2 = await client.GetAsync("/jwt/verify"); Assert.Equal(HttpStatusCode.OK, response2.StatusCode); } + + [Fact] + public async Task VerifyJwt_InvalidToken_Unauthorized() + { + await using var application = new AuthApplication(); + var client = application.CreateClient(); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "RandomToken"); + var response2 = await client.GetAsync("/jwt/verify"); + Assert.Equal(HttpStatusCode.Unauthorized, response2.StatusCode); + } } From f4ef96a38d5a147d4cba5c3ead7e2770b429c979 Mon Sep 17 00:00:00 2001 From: MadeYoga Date: Tue, 26 Jul 2022 16:53:33 +0800 Subject: [PATCH 3/5] Fix possible nullable warning in 2fa endpoint --- .../Endpoints/TwoFactorEndpointDefinition.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AuthEndpoints.MinimalApi/Endpoints/TwoFactorEndpointDefinition.cs b/src/AuthEndpoints.MinimalApi/Endpoints/TwoFactorEndpointDefinition.cs index a9a1d35..73770dc 100644 --- a/src/AuthEndpoints.MinimalApi/Endpoints/TwoFactorEndpointDefinition.cs +++ b/src/AuthEndpoints.MinimalApi/Endpoints/TwoFactorEndpointDefinition.cs @@ -116,7 +116,7 @@ public virtual async Task TwoStepVerificationLogin([FromBody] TwoStepVe // return BadRequest(); //} - TUser? user = await authenticator.Authenticate(request.Username, request.Password); + TUser? user = await authenticator.Authenticate(request.Username!, request.Password!); if (user == null) { From 9f61dd80f9ba668a6b275579fc2c9ceb621972b6 Mon Sep 17 00:00:00 2001 From: MadeYoga Date: Wed, 27 Jul 2022 12:28:51 +0800 Subject: [PATCH 4/5] Use core 1.4.4, fix possible null reference and update version --- src/AuthEndpoints/AuthEndpoints.csproj | 5 +- .../Controllers/BaseEndpointsController.cs | 347 ------------------ .../Controllers/JwtController.cs | 6 +- .../TwoStepVerificationController.cs | 2 +- 4 files changed, 7 insertions(+), 353 deletions(-) delete mode 100644 src/AuthEndpoints/Controllers/BaseEndpointsController.cs diff --git a/src/AuthEndpoints/AuthEndpoints.csproj b/src/AuthEndpoints/AuthEndpoints.csproj index 57e289d..c61515d 100644 --- a/src/AuthEndpoints/AuthEndpoints.csproj +++ b/src/AuthEndpoints/AuthEndpoints.csproj @@ -6,14 +6,14 @@ enable False A simple auth library for Asp.Net 6 that provides a set of web api controllers and minimal api endpoints to handle authentication actions - 1.4.7 + 1.4.8 yeogaa02 yeogaa02 https://github.com/madeyoga/AuthEndpoints README.md https://github.com/madeyoga/AuthEndpoints jwt;json-web-token;endpoints;asp-net-core;2fa - Using core 1.4.3 + Using core 1.4.4 and fix possible null reference warning True @@ -33,6 +33,7 @@ + diff --git a/src/AuthEndpoints/Controllers/BaseEndpointsController.cs b/src/AuthEndpoints/Controllers/BaseEndpointsController.cs deleted file mode 100644 index 7bce76d..0000000 --- a/src/AuthEndpoints/Controllers/BaseEndpointsController.cs +++ /dev/null @@ -1,347 +0,0 @@ -using AuthEndpoints.Models; -using AuthEndpoints.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using System.Security.Claims; -using System.Web; - -namespace AuthEndpoints.Controllers; - -/// -/// Inherit this base class to define endpoints that contain base authentication actions such as registration, set password, etc. -/// -/// -/// -[Obsolete("BaseEndpointsController<,> is deprecated, please use BaseAuthenticationController<,> instead.")] -[Route("users/")] -[ApiController] -public class BaseEndpointsController : ControllerBase - where TUserKey : IEquatable - where TUser : IdentityUser, new() -{ - protected readonly UserManager userManager; - protected readonly IdentityErrorDescriber errorDescriber; - protected readonly AuthEndpointsOptions options; - protected readonly IEmailSender emailSender; - protected readonly IEmailFactory emailFactory; - - public BaseEndpointsController(UserManager userManager, IdentityErrorDescriber errorDescriber, IOptions options, IEmailSender emailSender, IEmailFactory emailFactory) - { - this.userManager = userManager; - this.errorDescriber = errorDescriber; - this.options = options.Value; - this.emailSender = emailSender; - this.emailFactory = emailFactory; - } - - /// - /// Use this endpoint to register a new user - /// - [HttpPost("")] - public virtual async Task Register([FromBody] RegisterRequest request) - { - if (!ModelState.IsValid) - { - IEnumerable errors = ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)); - return BadRequest(new ErrorResponse(errors)); - } - - if (request.Password != request.ConfirmPassword) - { - return BadRequest(new ErrorResponse("Password not match confirm password.")); - } - - TUser registrationUser = new TUser() - { - Email = request.Email, - UserName = request.Username - }; - IdentityResult result = await userManager.CreateAsync(registrationUser, request.Password); - - if (!result.Succeeded) - { - IdentityError? primaryError = result.Errors.FirstOrDefault(); - - if (primaryError!.Code == nameof(errorDescriber.DuplicateEmail)) - { - return Conflict(new ErrorResponse("Email already exists.")); - } - else if (primaryError!.Code == nameof(errorDescriber.DuplicateUserName)) - { - return Conflict(new ErrorResponse("Username already exists.")); - } - else - { - return Conflict(new ErrorResponse($"Error code: {primaryError!.Code}\n{string.Join(", ", result.Errors.Select(e => e.Description))}")); - } - } - - return Ok(); - } - - /// - /// Use this endpoints to send email verification link via email - /// You should provide site in your frontend application (configured by ) - /// which will send POST request to verify email confirmation endpoint. - /// - /// - [Authorize(AuthenticationSchemes = "jwt")] - [HttpGet("verify_email")] - public virtual async Task EmailVerification() - { - string identity = HttpContext.User.FindFirstValue("id"); - TUser user = await userManager.FindByIdAsync(identity); - - if (user.EmailConfirmed) - { - return Unauthorized(); - } - - var token = await userManager.GenerateEmailConfirmationTokenAsync(user); - - var link = options.EmailConfirmationUrl! - .Replace("{uid}", identity) - .Replace("{token}", HttpUtility.UrlEncode(token)); - - // send email - var email = emailFactory.CreateConfirmationEmail( - new EmailData(new string[] { user.Email }, "Email Confirmation", link) - ); - await emailSender.SendEmailAsync(email).ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Use this endpoint to confirm user email. - /// - /// - /// - [HttpPost("verify_email_confirm")] - public virtual async Task EmailVerificationConfirm([FromBody] ConfirmEmailRequest request) - { - if (!ModelState.IsValid) - { - return BadRequestModelState(); - } - - if (request.Identity == null || request.Token == null) - { - return BadRequest(); - } - - var user = await userManager.FindByIdAsync(request.Identity); - - if (user == null) - { - return BadRequest(); - } - - if (user.EmailConfirmed) - { - return Unauthorized(); - } - - var result = await userManager.ConfirmEmailAsync(user, request.Token); - - if (result.Succeeded) - { - return NoContent(); - } - - IEnumerable errors = result.Errors.Select(error => error.Description); - return Conflict(errors); - } - - /// - /// Use this endpoint to retrieve the authenticated user - /// - [Authorize(AuthenticationSchemes = "jwt")] - [HttpGet("me")] - public virtual async Task GetMe() - { - if (!ModelState.IsValid) - { - return BadRequestModelState(); - } - - string identity = HttpContext.User.FindFirstValue("id"); - TUser currentUser = await userManager.FindByIdAsync(identity); - - return Ok(currentUser); - } - - /// - /// Use this endpoint to delete authenticated user. - /// - /// - [Authorize(AuthenticationSchemes = "jwt")] - [HttpDelete("delete")] - public virtual async Task Delete() - { - var identity = HttpContext.User.FindFirstValue("id"); - TUser user = await userManager.FindByIdAsync(identity); - var result = await userManager.DeleteAsync(user); - - if (result.Succeeded) - { - return NoContent(); - } - - IEnumerable errors = result.Errors.Select(error => error.Description); - return Conflict(errors); - } - - /// - /// Use this endpoint to change user's username - /// - /// - /// - [Authorize(AuthenticationSchemes = "jwt")] - [HttpPost("set_username")] - public virtual async Task SetUsername([FromBody] SetUsernameRequest request) - { - if (!ModelState.IsValid) - { - return BadRequestModelState(); - } - - string identity = HttpContext.User.FindFirstValue("id"); - TUser user = await userManager.FindByIdAsync(identity); - - if (await userManager.CheckPasswordAsync(user, request.CurrentPassword) is false) - { - return BadRequest(new ErrorResponse("Invalid current password")); - } - - user.UserName = request.NewUsername; - await userManager.UpdateAsync(user); - - return NoContent(); - } - - /// - /// Use this endpoint to change user password - /// - [Authorize(AuthenticationSchemes = "jwt")] - [HttpPost("set_password")] - public virtual async Task SetPassword([FromBody] SetPasswordRequest request) - { - if (!ModelState.IsValid) - { - return BadRequestModelState(); - } - - if (request.NewPassword != request.ConfirmNewPassword) - { - return BadRequest(new ErrorResponse("New password not match confirm password")); - } - - if (request.CurrentPassword == request.NewPassword) - { - return BadRequest(new ErrorResponse("New password cannot be the same as current password")); - } - - string identity = HttpContext.User.FindFirstValue("id"); - TUser user = await userManager.FindByIdAsync(identity); - - if (await userManager.CheckPasswordAsync(user, request.CurrentPassword) is false) - { - return BadRequest(new ErrorResponse("Invalid current password")); - } - - var token = await userManager.GeneratePasswordResetTokenAsync(user); - var result = await userManager.ResetPasswordAsync(user, token, request.NewPassword); - - if (!result.Succeeded) - { - return Conflict($"Error occured while updating password. Code: {result.Errors.First().Code}"); - } - - return NoContent(); - } - - /// - /// Use this endpoint to send email to user with password reset link. - /// You should provide site in your frontend application (configured by ) - /// which will send POST request to reset password confirmation endpoint. - /// - /// - /// - [HttpPost("reset_password")] - public virtual async Task ResetPassword([FromBody] ResetPasswordRequest request) - { - if (!ModelState.IsValid) - { - return BadRequestModelState(); - } - - TUser user = await userManager.FindByEmailAsync(request.Email); - - if (user == null) - { - return NotFound(); - } - - if (!user.EmailConfirmed) - { - return Unauthorized(); - } - - var token = await userManager.GeneratePasswordResetTokenAsync(user); - - // generate link to the frontend application that contains Identity and Token. - // e.g. "#password-reset/{uid}/{Token}" - var link = options.PasswordResetUrl! - .Replace("{uid}", user.Id.ToString()) - .Replace("{token}", HttpUtility.UrlEncode(token)) - .Trim(); - - // send email - var email = emailFactory.CreateResetPasswordEmail( - new EmailData(new string[] { user.Email }, "Password Reset", link) - ); - await emailSender.SendEmailAsync(email).ConfigureAwait(false); - - return NoContent(); - } - - /// - /// Use this endpoint to finish reset password process. - /// - /// - /// - [HttpPost("reset_password_confirm")] - public virtual async Task ResetPasswordConfirm([FromBody] ResetPasswordConfirmRequest request) - { - if (!ModelState.IsValid) - { - return BadRequestModelState(); - } - - TUser user = await userManager.FindByIdAsync(request.Identity); - - if (user == null) - { - return NotFound(); - } - - var result = await userManager.ResetPasswordAsync(user, request.Token, request.NewPassword); - - if (result.Succeeded) - { - return NoContent(); - } - - IEnumerable errors = result.Errors.Select(error => error.Description); - return Conflict(errors); - } - - private IActionResult BadRequestModelState() - { - IEnumerable errors = ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)); - return BadRequest(new ErrorResponse(errors)); - } -} diff --git a/src/AuthEndpoints/Controllers/JwtController.cs b/src/AuthEndpoints/Controllers/JwtController.cs index 3ca91c8..9b59662 100644 --- a/src/AuthEndpoints/Controllers/JwtController.cs +++ b/src/AuthEndpoints/Controllers/JwtController.cs @@ -50,7 +50,7 @@ public virtual async Task Create([FromBody] LoginRequest request) return BadRequestModelState(); } - TUser? user = await authenticator.Authenticate(request.Username, request.Password); + TUser? user = await authenticator.Authenticate(request.Username!, request.Password!); if (user == null) { @@ -82,7 +82,7 @@ public virtual async Task Refresh([FromBody] RefreshRequest reque return BadRequestModelState(); } - bool isValidRefreshToken = jwtValidator.Validate(request.RefreshToken, + bool isValidRefreshToken = jwtValidator.Validate(request.RefreshToken!, options.Value.RefreshValidationParameters!); if (!isValidRefreshToken) @@ -91,7 +91,7 @@ public virtual async Task Refresh([FromBody] RefreshRequest reque return BadRequest(new ErrorResponse("Invalid refresh token. Token may be expired or invalid.")); } - JwtSecurityToken jwt = jwtValidator.ReadJwtToken(request.RefreshToken); + JwtSecurityToken jwt = jwtValidator.ReadJwtToken(request.RefreshToken!); string userId = jwt.Claims.First(claim => claim.Type == "id").Value; TUser user = await userManager.FindByIdAsync(userId); diff --git a/src/AuthEndpoints/Controllers/TwoStepVerificationController.cs b/src/AuthEndpoints/Controllers/TwoStepVerificationController.cs index 5276afb..d79353e 100644 --- a/src/AuthEndpoints/Controllers/TwoStepVerificationController.cs +++ b/src/AuthEndpoints/Controllers/TwoStepVerificationController.cs @@ -119,7 +119,7 @@ public virtual async Task TwoStepVerificationLogin([FromBody] Two return BadRequest(); } - TUser? user = await authenticator.Authenticate(request.Username, request.Password); + TUser? user = await authenticator.Authenticate(request.Username!, request.Password!); if (user == null) { From 57dbfdece858a5100db8091442f1835fbf91a855 Mon Sep 17 00:00:00 2001 From: MadeYoga Date: Wed, 27 Jul 2022 12:32:22 +0800 Subject: [PATCH 5/5] Update release notes --- src/AuthEndpoints/AuthEndpoints.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AuthEndpoints/AuthEndpoints.csproj b/src/AuthEndpoints/AuthEndpoints.csproj index c61515d..0453177 100644 --- a/src/AuthEndpoints/AuthEndpoints.csproj +++ b/src/AuthEndpoints/AuthEndpoints.csproj @@ -13,7 +13,7 @@ README.md https://github.com/madeyoga/AuthEndpoints jwt;json-web-token;endpoints;asp-net-core;2fa - Using core 1.4.4 and fix possible null reference warning + Using core 1.4.4 and fix possible null reference warning and include minimal api True