1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-24 12:35:25 +01:00

Move identity endpoints to Identity service (#1807)

This commit is contained in:
Oscar Hinton 2022-01-17 13:21:51 +01:00 committed by GitHub
parent 56ee3bd290
commit 0def1830af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 246 additions and 20 deletions

View File

@ -81,8 +81,11 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb", "src\SharedWeb\SharedWeb.csproj", "{713D44C0-1BC1-4024-96A3-A98A49F33908}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb", "src\SharedWeb\SharedWeb.csproj", "{713D44C0-1BC1-4024-96A3-A98A49F33908}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.EntityFramework", "src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj", "{ED880735-0250-43C7-9662-FDC7C7416E7F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.EntityFramework", "src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj", "{ED880735-0250-43C7-9662-FDC7C7416E7F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Billing.Test", "test\Billing.Test\Billing.Test.csproj", "{B8639B10-2157-44BC-8CE1-D9EB4B50971F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Billing.Test", "test\Billing.Test\Billing.Test.csproj", "{B8639B10-2157-44BC-8CE1-D9EB4B50971F}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity.Test", "test\Identity.Test\Identity.Test.csproj", "{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -195,6 +198,10 @@ Global
{B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Debug|Any CPU.Build.0 = Debug|Any CPU {B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Release|Any CPU.ActiveCfg = Release|Any CPU {B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Release|Any CPU.Build.0 = Release|Any CPU {B8639B10-2157-44BC-8CE1-D9EB4B50971F}.Release|Any CPU.Build.0 = Release|Any CPU
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -226,6 +233,7 @@ Global
{713D44C0-1BC1-4024-96A3-A98A49F33908} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D} {713D44C0-1BC1-4024-96A3-A98A49F33908} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
{ED880735-0250-43C7-9662-FDC7C7416E7F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D} {ED880735-0250-43C7-9662-FDC7C7416E7F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
{B8639B10-2157-44BC-8CE1-D9EB4B50971F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {B8639B10-2157-44BC-8CE1-D9EB4B50971F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{310A1D8E-2D3F-4FA0-84D4-FFE31FCE193E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}

View File

@ -11,6 +11,8 @@ using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Enums.Provider; using Bit.Core.Enums.Provider;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Request.Accounts;
using Bit.Core.Models.Api.Response.Accounts;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -64,6 +66,9 @@ namespace Bit.Api.Controllers
_sendService = sendService; _sendService = sendService;
} }
#region DEPRECATED (Moved to Identity Service)
[Obsolete("2022-01-12 Moved to Identity, left for backwards compatability with older clients")]
[HttpPost("prelogin")] [HttpPost("prelogin")]
[AllowAnonymous] [AllowAnonymous]
public async Task<PreloginResponseModel> PostPrelogin([FromBody] PreloginRequestModel model) public async Task<PreloginResponseModel> PostPrelogin([FromBody] PreloginRequestModel model)
@ -74,12 +79,13 @@ namespace Bit.Api.Controllers
kdfInformation = new UserKdfInformation kdfInformation = new UserKdfInformation
{ {
Kdf = KdfType.PBKDF2_SHA256, Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 100000 KdfIterations = 100000,
}; };
} }
return new PreloginResponseModel(kdfInformation); return new PreloginResponseModel(kdfInformation);
} }
[Obsolete("2022-01-12 Moved to Identity, left for backwards compatability with older clients")]
[HttpPost("register")] [HttpPost("register")]
[AllowAnonymous] [AllowAnonymous]
[CaptchaProtected] [CaptchaProtected]
@ -101,6 +107,8 @@ namespace Bit.Api.Controllers
throw new BadRequestException(ModelState); throw new BadRequestException(ModelState);
} }
#endregion
[HttpPost("password-hint")] [HttpPost("password-hint")]
[AllowAnonymous] [AllowAnonymous]
public async Task PostPasswordHint([FromBody] PasswordHintRequestModel model) public async Task PostPasswordHint([FromBody] PasswordHintRequestModel model)

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Api.Request.Accounts;
namespace Bit.Api.Models.Request.Accounts namespace Bit.Api.Models.Request.Accounts
{ {

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Api.Request.Accounts;
namespace Bit.Api.Models.Request.Accounts namespace Bit.Api.Models.Request.Accounts
{ {

View File

@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities; using Bit.Core.Entities;
namespace Bit.Api.Models.Request.Accounts namespace Bit.Core.Models.Api.Request.Accounts
{ {
public class KeysRequestModel public class KeysRequestModel
{ {

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Models.Request.Accounts namespace Bit.Core.Models.Api.Request.Accounts
{ {
public class PreloginRequestModel public class PreloginRequestModel
{ {

View File

@ -3,11 +3,10 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Api;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Bit.Api.Models.Request.Accounts namespace Bit.Core.Models.Api.Request.Accounts
{ {
public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel
{ {

View File

@ -1,7 +1,7 @@
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
namespace Bit.Api.Models.Response namespace Bit.Core.Models.Api.Response.Accounts
{ {
public class PreloginResponseModel public class PreloginResponseModel
{ {

View File

@ -0,0 +1,70 @@
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Request.Accounts;
using Bit.Core.Models.Api.Response.Accounts;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Bit.Identity.Controllers
{
[Route("accounts")]
public class AccountsController : Controller
{
private readonly ILogger<AccountsController> _logger;
private readonly IUserRepository _userRepository;
private readonly IUserService _userService;
public AccountsController(
ILogger<AccountsController> logger,
IUserRepository userRepository,
IUserService userService)
{
_logger = logger;
_userRepository = userRepository;
_userService = userService;
}
// Moved from API, If you modify this endpoint, please update Identity as well.
[HttpPost("register")]
[CaptchaProtected]
public async Task PostRegister([FromBody] RegisterRequestModel model)
{
var result = await _userService.RegisterUserAsync(model.ToUser(), model.MasterPasswordHash,
model.Token, model.OrganizationUserId);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
{
ModelState.AddModelError(string.Empty, error.Description);
}
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
// Moved from API, If you modify this endpoint, please update Identity as well.
[HttpPost("prelogin")]
public async Task<PreloginResponseModel> PostPrelogin([FromBody] PreloginRequestModel model)
{
var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email);
if (kdfInformation == null)
{
kdfInformation = new UserKdfInformation
{
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 100000,
};
}
return new PreloginResponseModel(kdfInformation);
}
}
}

View File

@ -11,7 +11,6 @@ using Bit.Identity.Models;
using IdentityModel; using IdentityModel;
using IdentityServer4; using IdentityServer4;
using IdentityServer4.Services; using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Localization;
@ -20,31 +19,28 @@ using Microsoft.Extensions.Logging;
namespace Bit.Identity.Controllers namespace Bit.Identity.Controllers
{ {
public class AccountController : Controller // TODO: 2022-01-12, Remove account alias
[Route("account/[action]")]
[Route("sso/[action]")]
public class SsoController : Controller
{ {
private readonly IClientStore _clientStore;
private readonly IIdentityServerInteractionService _interaction; private readonly IIdentityServerInteractionService _interaction;
private readonly ILogger<AccountController> _logger; private readonly ILogger<SsoController> _logger;
private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IHttpClientFactory _clientFactory; private readonly IHttpClientFactory _clientFactory;
public AccountController( public SsoController(
IClientStore clientStore,
IIdentityServerInteractionService interaction, IIdentityServerInteractionService interaction,
ILogger<AccountController> logger, ILogger<SsoController> logger,
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
IUserRepository userRepository, IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IHttpClientFactory clientFactory) IHttpClientFactory clientFactory)
{ {
_clientStore = clientStore;
_interaction = interaction; _interaction = interaction;
_logger = logger; _logger = logger;
_ssoConfigRepository = ssoConfigRepository; _ssoConfigRepository = ssoConfigRepository;
_userRepository = userRepository; _userRepository = userRepository;
_organizationRepository = organizationRepository;
_clientFactory = clientFactory; _clientFactory = clientFactory;
} }
@ -272,7 +268,7 @@ namespace Bit.Identity.Controllers
} }
} }
public bool IsNativeClient(IdentityServer4.Models.AuthorizationRequest context) private bool IsNativeClient(IdentityServer4.Models.AuthorizationRequest context)
{ {
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal) return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal); && !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);

View File

@ -3,10 +3,10 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Api.Controllers; using Bit.Api.Controllers;
using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Request.Accounts;
using Bit.Core;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Request.Accounts;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -27,7 +27,6 @@ namespace Bit.Api.Test.Controllers
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPaymentService _paymentService; private readonly IPaymentService _paymentService;
private readonly ISsoUserRepository _ssoUserRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly ISendRepository _sendRepository; private readonly ISendRepository _sendRepository;

View File

@ -0,0 +1,115 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Request.Accounts;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Identity.Controllers;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
namespace Bit.Identity.Test.Controllers
{
public class AccountsControllerTests : IDisposable
{
private readonly AccountsController _sut;
private readonly ILogger<AccountsController> _logger;
private readonly IUserRepository _userRepository;
private readonly IUserService _userService;
public AccountsControllerTests()
{
_logger = Substitute.For<ILogger<AccountsController>>();
_userRepository = Substitute.For<IUserRepository>();
_userService = Substitute.For<IUserService>();
_sut = new AccountsController(
_logger,
_userRepository,
_userService
);
}
public void Dispose()
{
_sut?.Dispose();
}
[Fact]
public async Task PostPrelogin_WhenUserExists_ShouldReturnUserKdfInfo()
{
var userKdfInfo = new UserKdfInformation
{
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 5000
};
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult(userKdfInfo));
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
Assert.Equal(userKdfInfo.Kdf, response.Kdf);
Assert.Equal(userKdfInfo.KdfIterations, response.KdfIterations);
}
[Fact]
public async Task PostPrelogin_WhenUserDoesNotExist_ShouldDefaultToSha256And100000Iterations()
{
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult((UserKdfInformation)null));
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
Assert.Equal(KdfType.PBKDF2_SHA256, response.Kdf);
Assert.Equal(100000, response.KdfIterations);
}
[Fact]
public async Task PostRegister_ShouldRegisterUser()
{
var passwordHash = "abcdef";
var token = "123456";
var userGuid = new Guid();
_userService.RegisterUserAsync(Arg.Any<User>(), passwordHash, token, userGuid)
.Returns(Task.FromResult(IdentityResult.Success));
var request = new RegisterRequestModel
{
Name = "Example User",
Email = "user@example.com",
MasterPasswordHash = passwordHash,
MasterPasswordHint = "example",
Token = token,
OrganizationUserId = userGuid
};
await _sut.PostRegister(request);
await _userService.Received(1).RegisterUserAsync(Arg.Any<User>(), passwordHash, token, userGuid);
}
[Fact]
public async Task PostRegister_WhenUserServiceFails_ShouldThrowBadRequestException()
{
var passwordHash = "abcdef";
var token = "123456";
var userGuid = new Guid();
_userService.RegisterUserAsync(Arg.Any<User>(), passwordHash, token, userGuid)
.Returns(Task.FromResult(IdentityResult.Failed()));
var request = new RegisterRequestModel
{
Name = "Example User",
Email = "user@example.com",
MasterPasswordHash = passwordHash,
MasterPasswordHint = "example",
Token = token,
OrganizationUserId = userGuid
};
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostRegister(request));
}
}
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Core\Core.csproj" />
<ProjectReference Include="..\..\src\Identity\Identity.csproj" />
<ProjectReference Include="..\common\Common.csproj" />
</ItemGroup>
</Project>