Skip to content

Commit

Permalink
feat: change password functionality in Identity APIs added (#7)
Browse files Browse the repository at this point in the history
* added changepassword endpoint

* improved readme

* right align tables on readme

* syntax sugar
  • Loading branch information
thomasduft authored Oct 27, 2021
1 parent 043c17b commit 901d9a5
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 72 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
[![build](https://github.com/thomasduft/openiddict-ui/workflows/build/badge.svg)](https://github.com/thomasduft/openiddict-ui/actions) [![NuGet Release](https://img.shields.io/nuget/vpre/tomware.OpenIddict.UI.Api.svg)](https://www.nuget.org/packages/tomware.OpenIddict.UI.Api)


| Type | Description | Badge |
| :-------------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
| Build | Build status | [![build](https://github.com/thomasduft/openiddict-ui/workflows/build/badge.svg)](https://github.com/thomasduft/openiddict-ui/actions) |
| OpenIddict-UI API | API's for managing OpentIddict `Scopes` and `Applications`. | [![NuGet Release](https://img.shields.io/nuget/vpre/tomware.OpenIddict.UI.Api.svg)](https://www.nuget.org/packages/tomware.OpenIddict.UI.Api) |
| OpentIddict-UI Identity API | API's for managing [ASP.NET Core Identity](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-5.0&tabs=visual-studio) types (Accounts, Roles, etc.). | [![NuGet Release](https://img.shields.io/nuget/vpre/tomware.OpenIddict.UI.Identity.Api.svg)](https://www.nuget.org/packages/tomware.OpenIddict.UI.Identity.Api) |

# OpenIddict UI

Expand Down
8 changes: 8 additions & 0 deletions notes.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
TODO:

- organize APIs a bit more by feature
- i.e. identity -> AccountController
- AccountController
- RegisterUserController
- ChangePasswordController


- extract logic in AuthorizationController to service


- structuring
- suite (openiddict-ui-suite)
- Core
Expand Down
142 changes: 71 additions & 71 deletions samples/Server/Services/MigrationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,30 +44,31 @@ public async Task EnsureMigrationAsync()

await EnsureAdministratorRole(scope.ServiceProvider);
await EnsureAdministratorUser(scope.ServiceProvider);
}

static async Task RegisterApplicationsAsync(IServiceProvider provider)
{
var manager = provider.GetRequiredService<IOpenIddictApplicationManager>();
private static async Task RegisterApplicationsAsync(IServiceProvider provider)
{
var manager = provider.GetRequiredService<IOpenIddictApplicationManager>();

if (await manager.FindByClientIdAsync("spa_client") is null)
if (await manager.FindByClientIdAsync("spa_client") is null)
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "spa_client",
// ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ConsentType = ConsentTypes.Implicit,
DisplayName = "SPA Client Application",
PostLogoutRedirectUris =
ClientId = "spa_client",
// ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
ConsentType = ConsentTypes.Implicit,
DisplayName = "SPA Client Application",
PostLogoutRedirectUris =
{
new Uri("https://localhost:5000"),
new Uri("http://localhost:4200")
},
RedirectUris =
RedirectUris =
{
new Uri("https://localhost:5000"),
new Uri("http://localhost:4200")
},
Permissions =
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Logout,
Expand All @@ -81,95 +82,94 @@ await manager.CreateAsync(new OpenIddictApplicationDescriptor
Permissions.Prefixes.Scope + "server_scope",
Permissions.Prefixes.Scope + "api_scope"
},
Requirements =
Requirements =
{
Requirements.Features.ProofKeyForCodeExchange
}
});
}
});
}

if (await manager.FindByClientIdAsync("api_service") == null)
if (await manager.FindByClientIdAsync("api_service") == null)
{
var descriptor = new OpenIddictApplicationDescriptor
{
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "api_service",
DisplayName = "API Service",
ClientSecret = "my-api-secret",
Permissions =
ClientId = "api_service",
DisplayName = "API Service",
ClientSecret = "my-api-secret",
Permissions =
{
Permissions.Endpoints.Introspection
}
};
};

await manager.CreateAsync(descriptor);
}
await manager.CreateAsync(descriptor);
}
}

static async Task RegisterScopesAsync(IServiceProvider provider)
{
var manager = provider.GetRequiredService<IOpenIddictScopeManager>();
private static async Task RegisterScopesAsync(IServiceProvider provider)
{
var manager = provider.GetRequiredService<IOpenIddictScopeManager>();

if (await manager.FindByNameAsync("server_scope") is null)
if (await manager.FindByNameAsync("server_scope") is null)
{
await manager.CreateAsync(new OpenIddictScopeDescriptor
{
await manager.CreateAsync(new OpenIddictScopeDescriptor
{
Name = "server_scope",
DisplayName = "Server scope access",
Resources =
Name = "server_scope",
DisplayName = "Server scope access",
Resources =
{
"server"
}
});
}
});
}

if (await manager.FindByNameAsync("api_scope") == null)
if (await manager.FindByNameAsync("api_scope") == null)
{
var descriptor = new OpenIddictScopeDescriptor
{
var descriptor = new OpenIddictScopeDescriptor
{
Name = "api_scope",
DisplayName = "API Scope access",
Resources =
Name = "api_scope",
DisplayName = "API Scope access",
Resources =
{
"api_service"
}
};
};

await manager.CreateAsync(descriptor);
}
await manager.CreateAsync(descriptor);
}
}

static async Task EnsureAdministratorRole(IServiceProvider provider)
{
var manager = provider.GetRequiredService<RoleManager<IdentityRole>>();
private static async Task EnsureAdministratorRole(IServiceProvider provider)
{
var manager = provider.GetRequiredService<RoleManager<IdentityRole>>();

var role = Roles.ADMINISTRATOR_ROLE;
var roleExists = await manager.RoleExistsAsync(role);
if (!roleExists)
{
var newRole = new IdentityRole(role);
await manager.CreateAsync(newRole);
}
var role = Roles.ADMINISTRATOR_ROLE;
var roleExists = await manager.RoleExistsAsync(role);
if (!roleExists)
{
var newRole = new IdentityRole(role);
await manager.CreateAsync(newRole);
}
}

static async Task EnsureAdministratorUser(IServiceProvider provider)
{
var manager = provider.GetRequiredService<UserManager<ApplicationUser>>();
private static async Task EnsureAdministratorUser(IServiceProvider provider)
{
var manager = provider.GetRequiredService<UserManager<ApplicationUser>>();

var user = await manager.FindByNameAsync(Constants.ADMIN_MAILADDRESS);
if (user != null) return;
var user = await manager.FindByNameAsync(Constants.ADMIN_MAILADDRESS);
if (user != null) return;

var applicationUser = new ApplicationUser
{
UserName = Constants.ADMIN_MAILADDRESS,
Email = Constants.ADMIN_MAILADDRESS
};
var applicationUser = new ApplicationUser
{
UserName = Constants.ADMIN_MAILADDRESS,
Email = Constants.ADMIN_MAILADDRESS
};

var userResult = await manager.CreateAsync(applicationUser, "Pass123$");
if (!userResult.Succeeded) return;
var userResult = await manager.CreateAsync(applicationUser, "Pass123$");
if (!userResult.Succeeded) return;

await manager.SetLockoutEnabledAsync(applicationUser, false);
await manager.AddToRoleAsync(applicationUser, Roles.ADMINISTRATOR_ROLE);
}
await manager.SetLockoutEnabledAsync(applicationUser, false);
await manager.AddToRoleAsync(applicationUser, Roles.ADMINISTRATOR_ROLE);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace tomware.OpenIddict.UI.Identity.Api
public interface IAccountApiService
{
Task<IdentityResult> RegisterAsync(RegisterUserViewModel model);
Task<IdentityResult> ChangePasswordAsync(ChangePasswordViewModel model);
Task<IEnumerable<UserViewModel>> GetUsersAsync();
Task<UserViewModel> GetUserAsync(string id);
Task<IdentityResult> UpdateAsync(UserViewModel model);
Expand Down Expand Up @@ -46,6 +47,17 @@ RegisterUserViewModel model
return await _manager.CreateAsync(identiyUser, model.Password);
}

public async Task<IdentityResult> ChangePasswordAsync(ChangePasswordViewModel model)
{
var user = await _manager.FindByNameAsync(model.UserName);

return await _manager.ChangePasswordAsync(
user,
model.CurrentPassword,
model.NewPassword
);
}

public async Task<IEnumerable<UserViewModel>> GetUsersAsync()
{
// TODO: Paging ???
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ IAccountApiService service
[ProducesResponseType(typeof(IdentityResult), StatusCodes.Status200OK)]
public async Task<IActionResult> Register([FromBody] RegisterUserViewModel model)
{
if (model == null) return BadRequest();
if (ModelState.IsValid)
{
var result = await _service.RegisterAsync(model);
Expand All @@ -38,6 +39,25 @@ public async Task<IActionResult> Register([FromBody] RegisterUserViewModel model
return BadRequest(ModelState);
}

[HttpPost("changepassword")]
[ProducesResponseType(typeof(IdentityResult), StatusCodes.Status200OK)]
public async Task<IActionResult> ChangePassword([FromBody]ChangePasswordViewModel model)
{
if (model == null) return BadRequest();
if (ModelState.IsValid)
{
var result = await _service.ChangePasswordAsync(model);
if (result.Succeeded)
{
return Ok(result);
}

AddErrors(result);
}

return BadRequest(ModelState);
}

[HttpGet("users")]
[ProducesResponseType(typeof(IEnumerable<UserViewModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Users()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.ComponentModel.DataAnnotations;

namespace tomware.OpenIddict.UI.Identity.Api
{
public class ChangePasswordViewModel
{
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
public string CurrentPassword { get; set; }

[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
public string NewPassword { get; set; }

[Required]
[Compare("NewPassword", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }

[Required]
public string UserName { get; set; }
}
}
32 changes: 32 additions & 0 deletions tests/Integration/AccountApiTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,38 @@ public async Task Register_UserRegistered()
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

[Fact]
public async Task ChangePassword_PasswordChanged()
{
// Arrange
var email = "[email protected]";
await DeleteUser(email);

await PostAsync("/api/accounts/register", new RegisterUserViewModel
{
UserName = "username",
Email = email,
Password = "Pass123$",
ConfirmPassword = "Pass123$"
});

var user = await FindUserByEmail(email);
Assert.NotNull(user);

// Act
var response = await PostAsync($"/api/accounts/changepassword", new ChangePasswordViewModel
{
UserName = user.UserName,
CurrentPassword = "Pass123$",
NewPassword = "Pass1234$",
ConfirmPassword = "Pass1234$"
});

// Assert
Assert.NotNull(response);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

[Fact]
public async Task GetAsync_UserReceived()
{
Expand Down
29 changes: 29 additions & 0 deletions tests/Unit/AccountControllerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,35 @@ namespace tomware.OpenIddict.UI.Tests.Unit
{
public class AccountControllerTest
{
[Fact]
public async Task Register_WithNullModel_ReturnsBadRequest()
{
// Arrange
var controller = GetController();

// Act
var result = await controller.Register(null);

// Assert
Assert.NotNull(result);
Assert.IsType<BadRequestResult>(result);
}


[Fact]
public async Task ChangePassword_WithNullModel_ReturnsBadRequest()
{
// Arrange
var controller = GetController();

// Act
var result = await controller.ChangePassword(null);

// Assert
Assert.NotNull(result);
Assert.IsType<BadRequestResult>(result);
}

[Fact]
public async Task GetUser_WithNullId_ReturnsBadRequest()
{
Expand Down

0 comments on commit 901d9a5

Please sign in to comment.