mirror of
https://github.com/bitwarden/server.git
synced 2024-11-24 12:35:25 +01:00
[PM-6794] block legacy users from authN (#4088)
* block legacy users from authN * undo change to GetDeviceFromRequest * lint * add feature flag * format * add web vault url to error message * fix test * format
This commit is contained in:
parent
21a02054af
commit
b072fc56b1
@ -129,6 +129,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||||
public const string BulkDeviceApproval = "bulk-device-approval";
|
public const string BulkDeviceApproval = "bulk-device-approval";
|
||||||
public const string MemberAccessReport = "ac-2059-member-access-report";
|
public const string MemberAccessReport = "ac-2059-member-access-report";
|
||||||
|
public const string BlockLegacyUsers = "block-legacy-users";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -76,4 +76,10 @@ public interface IUserService
|
|||||||
Task SendOTPAsync(User user);
|
Task SendOTPAsync(User user);
|
||||||
Task<bool> VerifyOTPAsync(User user, string token);
|
Task<bool> VerifyOTPAsync(User user, string token);
|
||||||
Task<bool> VerifySecretAsync(User user, string secret);
|
Task<bool> VerifySecretAsync(User user, string secret);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the user is a legacy user. Legacy users use their master key as their encryption key.
|
||||||
|
/// We force these users to the web to migrate their encryption scheme.
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> IsLegacyUser(string userId);
|
||||||
}
|
}
|
||||||
|
@ -1304,6 +1304,28 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
return IdentityResult.Success;
|
return IdentityResult.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsLegacyUser(string userId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await FindByIdAsync(userId);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsLegacyUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IsLegacyUser(string)"/>
|
||||||
|
public static bool IsLegacyUser(User user)
|
||||||
|
{
|
||||||
|
return user.Key == null && user.MasterPassword != null && user.PrivateKey != null;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<IdentityResult> ValidatePasswordInternal(User user, string password)
|
private async Task<IdentityResult> ValidatePasswordInternal(User user, string password)
|
||||||
{
|
{
|
||||||
var errors = new List<IdentityError>();
|
var errors = new List<IdentityError>();
|
||||||
|
@ -162,6 +162,17 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
twoFactorToken = null;
|
twoFactorToken = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Force legacy users to the web for migration
|
||||||
|
if (FeatureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers))
|
||||||
|
{
|
||||||
|
if (UserService.IsLegacyUser(user) && request.ClientId != "web")
|
||||||
|
{
|
||||||
|
await FailAuthForLegacyUserAsync(user, context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Returns true if can finish validation process
|
// Returns true if can finish validation process
|
||||||
if (await IsValidAuthTypeAsync(user, request.GrantType))
|
if (await IsValidAuthTypeAsync(user, request.GrantType))
|
||||||
{
|
{
|
||||||
@ -184,6 +195,13 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async Task FailAuthForLegacyUserAsync(User user, T context)
|
||||||
|
{
|
||||||
|
await BuildErrorResultAsync(
|
||||||
|
$"Encryption key migration is required. Please log in to the web vault at {_globalSettings.BaseServiceUri.VaultWithHash}",
|
||||||
|
false, context, user);
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract Task<bool> ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext);
|
protected abstract Task<bool> ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext);
|
||||||
|
|
||||||
protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken)
|
protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken)
|
||||||
|
@ -13,6 +13,7 @@ using Bit.Core.Settings;
|
|||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Duende.IdentityServer.Extensions;
|
using Duende.IdentityServer.Extensions;
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
|
using HandlebarsDotNet;
|
||||||
using IdentityModel;
|
using IdentityModel;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
|
||||||
@ -57,6 +58,17 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
|
|
||||||
public async Task ValidateAsync(CustomTokenRequestValidationContext context)
|
public async Task ValidateAsync(CustomTokenRequestValidationContext context)
|
||||||
{
|
{
|
||||||
|
if (context.Result.ValidatedRequest.GrantType == "refresh_token")
|
||||||
|
{
|
||||||
|
// Force legacy users to the web for migration
|
||||||
|
if (await _userService.IsLegacyUser(GetSubject(context)?.GetSubjectId()) &&
|
||||||
|
context.Result.ValidatedRequest.ClientId != "web")
|
||||||
|
{
|
||||||
|
await FailAuthForLegacyUserAsync(null, context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
string[] allowedGrantTypes = { "authorization_code", "client_credentials" };
|
string[] allowedGrantTypes = { "authorization_code", "client_credentials" };
|
||||||
if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)
|
if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)
|
||||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("organization")
|
|| context.Result.ValidatedRequest.ClientId.StartsWith("organization")
|
||||||
@ -70,6 +82,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
context.Result.CustomResponse = new Dictionary<string, object> { { "encrypted_payload", payload } };
|
context.Result.CustomResponse = new Dictionary<string, object> { { "encrypted_payload", payload } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,6 +331,53 @@ public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
|
|||||||
await AssertDefaultTokenBodyAsync(context, "api");
|
await AssertDefaultTokenBodyAsync(context, "api");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task TokenEndpoint_GrantTypeClientCredentials_AsLegacyUser_NotOnWebClient_Fails(string deviceId)
|
||||||
|
{
|
||||||
|
var server = _factory.WithWebHostBuilder(builder =>
|
||||||
|
{
|
||||||
|
builder.UseSetting("globalSettings:launchDarkly:flagValues:block-legacy-users", "true");
|
||||||
|
}).Server;
|
||||||
|
|
||||||
|
var username = "test+tokenclientcredentials@email.com";
|
||||||
|
|
||||||
|
|
||||||
|
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
|
||||||
|
{
|
||||||
|
Email = username,
|
||||||
|
MasterPasswordHash = "master_password_hash"
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
var database = _factory.GetDatabaseContext();
|
||||||
|
var user = await database.Users
|
||||||
|
.FirstAsync(u => u.Email == username);
|
||||||
|
|
||||||
|
user.PrivateKey = "EncryptedPrivateKey";
|
||||||
|
await database.SaveChangesAsync();
|
||||||
|
|
||||||
|
var context = await server.PostAsync("/connect/token", new FormUrlEncodedContent(
|
||||||
|
new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "scope", "api offline_access" },
|
||||||
|
{ "client_id", "browser" },
|
||||||
|
{ "deviceType", DeviceTypeAsString(DeviceType.ChromeBrowser) },
|
||||||
|
{ "deviceIdentifier", deviceId },
|
||||||
|
{ "deviceName", "chrome" },
|
||||||
|
{ "grant_type", "password" },
|
||||||
|
{ "username", username },
|
||||||
|
{ "password", "master_password_hash" },
|
||||||
|
}), context => context.SetAuthEmail(username));
|
||||||
|
|
||||||
|
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||||
|
|
||||||
|
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||||
|
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "ErrorModel", JsonValueKind.Object);
|
||||||
|
var message = AssertHelper.AssertJsonProperty(error, "Message", JsonValueKind.String).GetString();
|
||||||
|
Assert.StartsWith("Encryption key migration is required.", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_Success(Organization organization, Bit.Core.Entities.OrganizationApiKey organizationApiKey)
|
public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_Success(Organization organization, Bit.Core.Entities.OrganizationApiKey organizationApiKey)
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user