mirror of
https://github.com/bitwarden/server.git
synced 2024-12-28 17:57:37 +01:00
sso integrations (#822)
* stub out hybrid sso * support for PKCE authorization_code clients * sso service urls * sso client key * abstract request validator * support for verifying password * custom AuthorizationCodeStore that does not remove codes * cleanup * comment * created master password * ResetMasterPassword * rename Sso client to OidcIdentity * update env builder * bitwarden sso project in docker-compose * sso path in nginx config
This commit is contained in:
parent
2742b414fd
commit
0d0c6c7167
@ -6,11 +6,13 @@
|
|||||||
"identity": "https://identity.bitwarden.com",
|
"identity": "https://identity.bitwarden.com",
|
||||||
"admin": "https://admin.bitwarden.com",
|
"admin": "https://admin.bitwarden.com",
|
||||||
"notifications": "https://notifications.bitwarden.com",
|
"notifications": "https://notifications.bitwarden.com",
|
||||||
|
"sso": "https://sso.bitwarden.com",
|
||||||
"internalNotifications": "https://notifications.bitwarden.com",
|
"internalNotifications": "https://notifications.bitwarden.com",
|
||||||
"internalAdmin": "https://admin.bitwarden.com",
|
"internalAdmin": "https://admin.bitwarden.com",
|
||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com"
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
|
"internalSso": "https://sso.bitwarden.com"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
|
@ -10,11 +10,13 @@
|
|||||||
"identity": "http://localhost:33656",
|
"identity": "http://localhost:33656",
|
||||||
"admin": "http://localhost:62911",
|
"admin": "http://localhost:62911",
|
||||||
"notifications": "http://localhost:61840",
|
"notifications": "http://localhost:61840",
|
||||||
|
"sso": "http://localhost:51822",
|
||||||
"internalNotifications": "http://localhost:61840",
|
"internalNotifications": "http://localhost:61840",
|
||||||
"internalAdmin": "http://localhost:62911",
|
"internalAdmin": "http://localhost:62911",
|
||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "http://localhost:4001"
|
"internalVault": "http://localhost:4001",
|
||||||
|
"internalSso": "http://localhost:51822"
|
||||||
},
|
},
|
||||||
"sqlServer": {
|
"sqlServer": {
|
||||||
"connectionString": "SECRET"
|
"connectionString": "SECRET"
|
||||||
|
@ -195,6 +195,25 @@ namespace Bit.Api.Controllers
|
|||||||
throw new BadRequestException(ModelState);
|
throw new BadRequestException(ModelState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("verify-password")]
|
||||||
|
public async Task PostVerifyPassword([FromBody]VerifyPasswordRequestModel model)
|
||||||
|
{
|
||||||
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelState.AddModelError(nameof(model.MasterPasswordHash), "Invalid password.");
|
||||||
|
await Task.Delay(2000);
|
||||||
|
throw new BadRequestException(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("kdf")]
|
[HttpPost("kdf")]
|
||||||
public async Task PostKdf([FromBody]KdfRequestModel model)
|
public async Task PostKdf([FromBody]KdfRequestModel model)
|
||||||
{
|
{
|
||||||
@ -633,7 +652,7 @@ namespace Bit.Api.Controllers
|
|||||||
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("tax")]
|
[HttpGet("tax")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<TaxInfoResponseModel> GetTaxInfo()
|
public async Task<TaxInfoResponseModel> GetTaxInfo()
|
||||||
@ -647,7 +666,7 @@ namespace Bit.Api.Controllers
|
|||||||
var taxInfo = await _paymentService.GetTaxInfoAsync(user);
|
var taxInfo = await _paymentService.GetTaxInfoAsync(user);
|
||||||
return new TaxInfoResponseModel(taxInfo);
|
return new TaxInfoResponseModel(taxInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("tax")]
|
[HttpPut("tax")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task PutTaxInfo([FromBody]TaxInfoUpdateRequestModel model)
|
public async Task PutTaxInfo([FromBody]TaxInfoUpdateRequestModel model)
|
||||||
|
@ -78,13 +78,13 @@ namespace Bit.Api
|
|||||||
config.AddPolicy("Application", policy =>
|
config.AddPolicy("Application", policy =>
|
||||||
{
|
{
|
||||||
policy.RequireAuthenticatedUser();
|
policy.RequireAuthenticatedUser();
|
||||||
policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application");
|
policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external");
|
||||||
policy.RequireClaim(JwtClaimTypes.Scope, "api");
|
policy.RequireClaim(JwtClaimTypes.Scope, "api");
|
||||||
});
|
});
|
||||||
config.AddPolicy("Web", policy =>
|
config.AddPolicy("Web", policy =>
|
||||||
{
|
{
|
||||||
policy.RequireAuthenticatedUser();
|
policy.RequireAuthenticatedUser();
|
||||||
policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application");
|
policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external");
|
||||||
policy.RequireClaim(JwtClaimTypes.Scope, "api");
|
policy.RequireClaim(JwtClaimTypes.Scope, "api");
|
||||||
policy.RequireClaim(JwtClaimTypes.ClientId, "web");
|
policy.RequireClaim(JwtClaimTypes.ClientId, "web");
|
||||||
});
|
});
|
||||||
|
@ -6,11 +6,13 @@
|
|||||||
"identity": "https://identity.bitwarden.com",
|
"identity": "https://identity.bitwarden.com",
|
||||||
"admin": "https://admin.bitwarden.com",
|
"admin": "https://admin.bitwarden.com",
|
||||||
"notifications": "https://notifications.bitwarden.com",
|
"notifications": "https://notifications.bitwarden.com",
|
||||||
|
"sso": "https://sso.bitwarden.com",
|
||||||
"internalNotifications": "https://notifications.bitwarden.com",
|
"internalNotifications": "https://notifications.bitwarden.com",
|
||||||
"internalAdmin": "https://admin.bitwarden.com",
|
"internalAdmin": "https://admin.bitwarden.com",
|
||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com"
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
|
"internalSso": "https://sso.bitwarden.com"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
|
@ -10,11 +10,13 @@
|
|||||||
"identity": "http://localhost:33656",
|
"identity": "http://localhost:33656",
|
||||||
"admin": "http://localhost:62911",
|
"admin": "http://localhost:62911",
|
||||||
"notifications": "http://localhost:61840",
|
"notifications": "http://localhost:61840",
|
||||||
|
"sso": "http://localhost:51822",
|
||||||
"internalNotifications": "http://localhost:61840",
|
"internalNotifications": "http://localhost:61840",
|
||||||
"internalAdmin": "http://localhost:62911",
|
"internalAdmin": "http://localhost:62911",
|
||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "http://localhost:4001"
|
"internalVault": "http://localhost:4001",
|
||||||
|
"internalSso": "http://localhost:51822"
|
||||||
},
|
},
|
||||||
"sqlServer": {
|
"sqlServer": {
|
||||||
"connectionString": "SECRET"
|
"connectionString": "SECRET"
|
||||||
|
@ -6,11 +6,13 @@
|
|||||||
"identity": "https://identity.bitwarden.com",
|
"identity": "https://identity.bitwarden.com",
|
||||||
"admin": "https://admin.bitwarden.com",
|
"admin": "https://admin.bitwarden.com",
|
||||||
"notifications": "https://notifications.bitwarden.com",
|
"notifications": "https://notifications.bitwarden.com",
|
||||||
|
"sso": "https://sso.bitwarden.com",
|
||||||
"internalNotifications": "https://notifications.bitwarden.com",
|
"internalNotifications": "https://notifications.bitwarden.com",
|
||||||
"internalAdmin": "https://admin.bitwarden.com",
|
"internalAdmin": "https://admin.bitwarden.com",
|
||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com"
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
|
"internalSso": "https://sso.bitwarden.com"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
|
@ -10,11 +10,13 @@
|
|||||||
"identity": "http://localhost:33656",
|
"identity": "http://localhost:33656",
|
||||||
"admin": "http://localhost:62911",
|
"admin": "http://localhost:62911",
|
||||||
"notifications": "http://localhost:61840",
|
"notifications": "http://localhost:61840",
|
||||||
|
"sso": "http://localhost:51822",
|
||||||
"internalNotifications": "http://localhost:61840",
|
"internalNotifications": "http://localhost:61840",
|
||||||
"internalAdmin": "http://localhost:62911",
|
"internalAdmin": "http://localhost:62911",
|
||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "http://localhost:4001"
|
"internalVault": "http://localhost:4001",
|
||||||
|
"internalSso": "http://localhost:51822"
|
||||||
},
|
},
|
||||||
"sqlServer": {
|
"sqlServer": {
|
||||||
"connectionString": "SECRET"
|
"connectionString": "SECRET"
|
||||||
|
@ -15,6 +15,7 @@ namespace Bit.Core
|
|||||||
public string LicenseCertificatePassword { get; set; }
|
public string LicenseCertificatePassword { get; set; }
|
||||||
public virtual string PushRelayBaseUri { get; set; }
|
public virtual string PushRelayBaseUri { get; set; }
|
||||||
public virtual string InternalIdentityKey { get; set; }
|
public virtual string InternalIdentityKey { get; set; }
|
||||||
|
public virtual string OidcIdentityClientKey { get; set; }
|
||||||
public virtual string HibpApiKey { get; set; }
|
public virtual string HibpApiKey { get; set; }
|
||||||
public virtual bool DisableUserRegistration { get; set; }
|
public virtual bool DisableUserRegistration { get; set; }
|
||||||
public virtual bool DisableEmailNewDevice { get; set; }
|
public virtual bool DisableEmailNewDevice { get; set; }
|
||||||
@ -50,11 +51,13 @@ namespace Bit.Core
|
|||||||
public string Identity { get; set; }
|
public string Identity { get; set; }
|
||||||
public string Admin { get; set; }
|
public string Admin { get; set; }
|
||||||
public string Notifications { get; set; }
|
public string Notifications { get; set; }
|
||||||
|
public string Sso { get; set; }
|
||||||
public string InternalNotifications { get; set; }
|
public string InternalNotifications { get; set; }
|
||||||
public string InternalAdmin { get; set; }
|
public string InternalAdmin { get; set; }
|
||||||
public string InternalIdentity { get; set; }
|
public string InternalIdentity { get; set; }
|
||||||
public string InternalApi { get; set; }
|
public string InternalApi { get; set; }
|
||||||
public string InternalVault { get; set; }
|
public string InternalVault { get; set; }
|
||||||
|
public string InternalSso { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SqlSettings
|
public class SqlSettings
|
||||||
|
44
src/Core/IdentityServer/AuthorizationCodeStore.cs
Normal file
44
src/Core/IdentityServer/AuthorizationCodeStore.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using IdentityServer4;
|
||||||
|
using IdentityServer4.Extensions;
|
||||||
|
using IdentityServer4.Models;
|
||||||
|
using IdentityServer4.Services;
|
||||||
|
using IdentityServer4.Stores;
|
||||||
|
using IdentityServer4.Stores.Serialization;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.Core.IdentityServer
|
||||||
|
{
|
||||||
|
// ref: https://raw.githubusercontent.com/IdentityServer/IdentityServer4/3.1.3/src/IdentityServer4/src/Stores/Default/DefaultAuthorizationCodeStore.cs
|
||||||
|
public class AuthorizationCodeStore : DefaultGrantStore<AuthorizationCode>, IAuthorizationCodeStore
|
||||||
|
{
|
||||||
|
public AuthorizationCodeStore(
|
||||||
|
IPersistedGrantStore store,
|
||||||
|
IPersistentGrantSerializer serializer,
|
||||||
|
IHandleGenerationService handleGenerationService,
|
||||||
|
ILogger<DefaultAuthorizationCodeStore> logger)
|
||||||
|
: base(IdentityServerConstants.PersistedGrantTypes.AuthorizationCode, store, serializer,
|
||||||
|
handleGenerationService, logger)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public Task<string> StoreAuthorizationCodeAsync(AuthorizationCode code)
|
||||||
|
{
|
||||||
|
return CreateItemAsync(code, code.ClientId, code.Subject.GetSubjectId(), code.CreationTime, code.Lifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<AuthorizationCode> GetAuthorizationCodeAsync(string code)
|
||||||
|
{
|
||||||
|
return GetItemAsync(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RemoveAuthorizationCodeAsync(string code)
|
||||||
|
{
|
||||||
|
// return RemoveItemAsync(code);
|
||||||
|
|
||||||
|
// We don't want to delete authorization codes during validation.
|
||||||
|
// We'll rely on the authorization code lifecycle for short term validation and the
|
||||||
|
// DatabaseExpiredGrantsJob to clean up old authorization codes.
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
416
src/Core/IdentityServer/BaseRequestValidator.cs
Normal file
416
src/Core/IdentityServer/BaseRequestValidator.cs
Normal file
@ -0,0 +1,416 @@
|
|||||||
|
using Bit.Core.Models.Table;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using IdentityServer4.Validation;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using System.Linq;
|
||||||
|
using Bit.Core.Models;
|
||||||
|
using Bit.Core.Identity;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Bit.Core.Models.Api;
|
||||||
|
|
||||||
|
namespace Bit.Core.IdentityServer
|
||||||
|
{
|
||||||
|
public abstract class BaseRequestValidator<T> where T : class
|
||||||
|
{
|
||||||
|
private UserManager<User> _userManager;
|
||||||
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
|
private readonly IDeviceService _deviceService;
|
||||||
|
private readonly IUserService _userService;
|
||||||
|
private readonly IEventService _eventService;
|
||||||
|
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
|
private readonly IMailService _mailService;
|
||||||
|
private readonly ILogger<ResourceOwnerPasswordValidator> _logger;
|
||||||
|
private readonly CurrentContext _currentContext;
|
||||||
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
|
||||||
|
public BaseRequestValidator(
|
||||||
|
UserManager<User> userManager,
|
||||||
|
IDeviceRepository deviceRepository,
|
||||||
|
IDeviceService deviceService,
|
||||||
|
IUserService userService,
|
||||||
|
IEventService eventService,
|
||||||
|
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IApplicationCacheService applicationCacheService,
|
||||||
|
IMailService mailService,
|
||||||
|
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||||
|
CurrentContext currentContext,
|
||||||
|
GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_deviceRepository = deviceRepository;
|
||||||
|
_deviceService = deviceService;
|
||||||
|
_userService = userService;
|
||||||
|
_eventService = eventService;
|
||||||
|
_organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_applicationCacheService = applicationCacheService;
|
||||||
|
_mailService = mailService;
|
||||||
|
_logger = logger;
|
||||||
|
_currentContext = currentContext;
|
||||||
|
_globalSettings = globalSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task ValidateAsync(T context, ValidatedTokenRequest request)
|
||||||
|
{
|
||||||
|
var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
|
||||||
|
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
|
||||||
|
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
|
||||||
|
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
||||||
|
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
||||||
|
|
||||||
|
var (user, valid) = await ValidateContextAsync(context);
|
||||||
|
if (!valid)
|
||||||
|
{
|
||||||
|
await BuildErrorResultAsync(false, context, user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var twoFactorRequirement = await RequiresTwoFactorAsync(user);
|
||||||
|
if (twoFactorRequirement.Item1)
|
||||||
|
{
|
||||||
|
var twoFactorProviderType = TwoFactorProviderType.Authenticator; // Just defaulting it
|
||||||
|
if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out twoFactorProviderType))
|
||||||
|
{
|
||||||
|
await BuildTwoFactorResultAsync(user, twoFactorRequirement.Item2, context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var verified = await VerifyTwoFactor(user, twoFactorRequirement.Item2,
|
||||||
|
twoFactorProviderType, twoFactorToken);
|
||||||
|
if (!verified && twoFactorProviderType != TwoFactorProviderType.Remember)
|
||||||
|
{
|
||||||
|
await BuildErrorResultAsync(true, context, user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (!verified && twoFactorProviderType == TwoFactorProviderType.Remember)
|
||||||
|
{
|
||||||
|
await Task.Delay(2000); // Delay for brute force.
|
||||||
|
await BuildTwoFactorResultAsync(user, twoFactorRequirement.Item2, context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
twoFactorRequest = false;
|
||||||
|
twoFactorRemember = false;
|
||||||
|
twoFactorToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var device = await SaveDeviceAsync(user, request);
|
||||||
|
await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract Task<(User, bool)> ValidateContextAsync(T context);
|
||||||
|
|
||||||
|
protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken)
|
||||||
|
{
|
||||||
|
await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn);
|
||||||
|
|
||||||
|
var claims = new List<Claim>();
|
||||||
|
|
||||||
|
if (device != null)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim("device", device.Identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
var customResponse = new Dictionary<string, object>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
|
||||||
|
{
|
||||||
|
customResponse.Add("PrivateKey", user.PrivateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(user.Key))
|
||||||
|
{
|
||||||
|
customResponse.Add("Key", user.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword));
|
||||||
|
customResponse.Add("Kdf", (byte)user.Kdf);
|
||||||
|
customResponse.Add("KdfIterations", user.KdfIterations);
|
||||||
|
|
||||||
|
if (sendRememberToken)
|
||||||
|
{
|
||||||
|
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
|
||||||
|
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));
|
||||||
|
customResponse.Add("TwoFactorToken", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetSuccessResult(context, user, claims, customResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context)
|
||||||
|
{
|
||||||
|
var providerKeys = new List<byte>();
|
||||||
|
var providers = new Dictionary<byte, Dictionary<string, object>>();
|
||||||
|
|
||||||
|
var enabledProviders = new List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>();
|
||||||
|
if (organization?.GetTwoFactorProviders() != null)
|
||||||
|
{
|
||||||
|
enabledProviders.AddRange(organization.GetTwoFactorProviders().Where(
|
||||||
|
p => organization.TwoFactorProviderIsEnabled(p.Key)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.GetTwoFactorProviders() != null)
|
||||||
|
{
|
||||||
|
foreach (var p in user.GetTwoFactorProviders())
|
||||||
|
{
|
||||||
|
if (await _userService.TwoFactorProviderIsEnabledAsync(p.Key, user))
|
||||||
|
{
|
||||||
|
enabledProviders.Add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enabledProviders.Any())
|
||||||
|
{
|
||||||
|
await BuildErrorResultAsync(false, context, user);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var provider in enabledProviders)
|
||||||
|
{
|
||||||
|
providerKeys.Add((byte)provider.Key);
|
||||||
|
var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value);
|
||||||
|
providers.Add((byte)provider.Key, infoDict);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetTwoFactorResult(context,
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "TwoFactorProviders", providers.Keys },
|
||||||
|
{ "TwoFactorProviders2", providers }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
|
||||||
|
{
|
||||||
|
// Send email now if this is their only 2FA method
|
||||||
|
await _userService.SendTwoFactorEmailAsync(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task BuildErrorResultAsync(bool twoFactorRequest, T context, User user)
|
||||||
|
{
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
await _eventService.LogUserEventAsync(user.Id,
|
||||||
|
twoFactorRequest ? EventType.User_FailedLogIn2fa : EventType.User_FailedLogIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(Constants.BypassFiltersEventId,
|
||||||
|
string.Format("Failed login attempt{0}{1}", twoFactorRequest ? ", 2FA invalid." : ".",
|
||||||
|
$" {_currentContext.IpAddress}"));
|
||||||
|
}
|
||||||
|
await Task.Delay(2000); // Delay for brute force.
|
||||||
|
SetErrorResult(context,
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{{
|
||||||
|
"ErrorModel", new ErrorResponseModel(twoFactorRequest ?
|
||||||
|
"Two-step token is invalid. Try again." : "Username or password is incorrect. Try again.")
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void SetTwoFactorResult(T context, Dictionary<string, object> customResponse);
|
||||||
|
|
||||||
|
protected abstract void SetSuccessResult(T context, User user, List<Claim> claims,
|
||||||
|
Dictionary<string, object> customResponse);
|
||||||
|
|
||||||
|
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
|
||||||
|
|
||||||
|
private async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user)
|
||||||
|
{
|
||||||
|
var individualRequired = _userManager.SupportsUserTwoFactor &&
|
||||||
|
await _userManager.GetTwoFactorEnabledAsync(user) &&
|
||||||
|
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
|
||||||
|
|
||||||
|
Organization firstEnabledOrg = null;
|
||||||
|
var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))
|
||||||
|
.ToList();
|
||||||
|
if (orgs.Any())
|
||||||
|
{
|
||||||
|
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||||
|
var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id));
|
||||||
|
if (twoFactorOrgs.Any())
|
||||||
|
{
|
||||||
|
var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
||||||
|
firstEnabledOrg = userOrgs.FirstOrDefault(
|
||||||
|
o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Tuple<bool, Organization>(individualRequired || firstEnabledOrg != null, firstEnabledOrg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
|
||||||
|
{
|
||||||
|
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
|
||||||
|
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Device GetDeviceFromRequest(ValidatedRequest request)
|
||||||
|
{
|
||||||
|
var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString();
|
||||||
|
var deviceType = request.Raw["DeviceType"]?.ToString();
|
||||||
|
var deviceName = request.Raw["DeviceName"]?.ToString();
|
||||||
|
var devicePushToken = request.Raw["DevicePushToken"]?.ToString();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(deviceIdentifier) || string.IsNullOrWhiteSpace(deviceType) ||
|
||||||
|
string.IsNullOrWhiteSpace(deviceName) || !Enum.TryParse(deviceType, out DeviceType type))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Device
|
||||||
|
{
|
||||||
|
Identifier = deviceIdentifier,
|
||||||
|
Name = deviceName,
|
||||||
|
Type = type,
|
||||||
|
PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type,
|
||||||
|
string token)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case TwoFactorProviderType.Authenticator:
|
||||||
|
case TwoFactorProviderType.Email:
|
||||||
|
case TwoFactorProviderType.Duo:
|
||||||
|
case TwoFactorProviderType.YubiKey:
|
||||||
|
case TwoFactorProviderType.U2f:
|
||||||
|
case TwoFactorProviderType.Remember:
|
||||||
|
if (type != TwoFactorProviderType.Remember &&
|
||||||
|
!(await _userService.TwoFactorProviderIsEnabledAsync(type, user)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return await _userManager.VerifyTwoFactorTokenAsync(user,
|
||||||
|
CoreHelpers.CustomProviderName(type), token);
|
||||||
|
case TwoFactorProviderType.OrganizationDuo:
|
||||||
|
if (!organization?.TwoFactorProviderIsEnabled(type) ?? true)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<string, object>> BuildTwoFactorParams(Organization organization, User user,
|
||||||
|
TwoFactorProviderType type, TwoFactorProvider provider)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case TwoFactorProviderType.Duo:
|
||||||
|
case TwoFactorProviderType.U2f:
|
||||||
|
case TwoFactorProviderType.Email:
|
||||||
|
case TwoFactorProviderType.YubiKey:
|
||||||
|
if (!(await _userService.TwoFactorProviderIsEnabledAsync(type, user)))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
|
||||||
|
CoreHelpers.CustomProviderName(type));
|
||||||
|
if (type == TwoFactorProviderType.Duo)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["Host"] = provider.MetaData["Host"],
|
||||||
|
["Signature"] = token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (type == TwoFactorProviderType.U2f)
|
||||||
|
{
|
||||||
|
// TODO: Remove "Challenges" in a future update. Deprecated.
|
||||||
|
var tokens = token?.Split('|');
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["Challenge"] = tokens != null && tokens.Length > 0 ? tokens[0] : null,
|
||||||
|
["Challenges"] = tokens != null && tokens.Length > 1 ? tokens[1] : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (type == TwoFactorProviderType.Email)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["Email"] = token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (type == TwoFactorProviderType.YubiKey)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["Nfc"] = (bool)provider.MetaData["Nfc"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
case TwoFactorProviderType.OrganizationDuo:
|
||||||
|
if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["Host"] = provider.MetaData["Host"],
|
||||||
|
["Signature"] = await _organizationDuoWebTokenProvider.GenerateAsync(organization, user)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
|
||||||
|
{
|
||||||
|
var device = GetDeviceFromRequest(request);
|
||||||
|
if (device != null)
|
||||||
|
{
|
||||||
|
var existingDevice = await _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id);
|
||||||
|
if (existingDevice == null)
|
||||||
|
{
|
||||||
|
device.UserId = user.Id;
|
||||||
|
await _deviceService.SaveAsync(device);
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (now - user.CreationDate > TimeSpan.FromMinutes(10))
|
||||||
|
{
|
||||||
|
var deviceType = device.Type.GetType().GetMember(device.Type.ToString())
|
||||||
|
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
|
||||||
|
if (!_globalSettings.DisableEmailNewDevice)
|
||||||
|
{
|
||||||
|
await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,
|
||||||
|
_currentContext.IpAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
src/Core/IdentityServer/CustomTokenRequestValidator.cs
Normal file
89
src/Core/IdentityServer/CustomTokenRequestValidator.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
using Bit.Core.Models.Table;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using IdentityServer4.Validation;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using System.Linq;
|
||||||
|
using Bit.Core.Identity;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using IdentityServer4.Extensions;
|
||||||
|
|
||||||
|
namespace Bit.Core.IdentityServer
|
||||||
|
{
|
||||||
|
public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>,
|
||||||
|
ICustomTokenRequestValidator
|
||||||
|
{
|
||||||
|
private UserManager<User> _userManager;
|
||||||
|
|
||||||
|
public CustomTokenRequestValidator(
|
||||||
|
UserManager<User> userManager,
|
||||||
|
IDeviceRepository deviceRepository,
|
||||||
|
IDeviceService deviceService,
|
||||||
|
IUserService userService,
|
||||||
|
IEventService eventService,
|
||||||
|
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IApplicationCacheService applicationCacheService,
|
||||||
|
IMailService mailService,
|
||||||
|
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||||
|
CurrentContext currentContext,
|
||||||
|
GlobalSettings globalSettings)
|
||||||
|
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||||
|
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
||||||
|
applicationCacheService, mailService, logger, currentContext, globalSettings)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ValidateAsync(CustomTokenRequestValidationContext context)
|
||||||
|
{
|
||||||
|
if (context.Result.ValidatedRequest.GrantType != "authorization_code")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ValidateAsync(context, context.Result.ValidatedRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async override Task<(User, bool)> ValidateContextAsync(CustomTokenRequestValidationContext context)
|
||||||
|
{
|
||||||
|
var user = await _userManager.FindByEmailAsync(context.Result.ValidatedRequest.Subject.GetDisplayName());
|
||||||
|
return (user, user != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SetSuccessResult(CustomTokenRequestValidationContext context, User user,
|
||||||
|
List<Claim> claims, Dictionary<string, object> customResponse)
|
||||||
|
{
|
||||||
|
context.Result.CustomResponse = customResponse;
|
||||||
|
if (claims?.Any() ?? false)
|
||||||
|
{
|
||||||
|
context.Result.ValidatedRequest.Client.AlwaysSendClientClaims = true;
|
||||||
|
context.Result.ValidatedRequest.Client.ClientClaimsPrefix = string.Empty;
|
||||||
|
foreach (var claim in claims)
|
||||||
|
{
|
||||||
|
context.Result.ValidatedRequest.ClientClaims.Add(claim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SetTwoFactorResult(CustomTokenRequestValidationContext context,
|
||||||
|
Dictionary<string, object> customResponse)
|
||||||
|
{
|
||||||
|
context.Result.Error = "invalid_grant";
|
||||||
|
context.Result.ErrorDescription = "Two factor required.";
|
||||||
|
context.Result.IsError = true;
|
||||||
|
context.Result.CustomResponse = customResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SetErrorResult(CustomTokenRequestValidationContext context,
|
||||||
|
Dictionary<string, object> customResponse)
|
||||||
|
{
|
||||||
|
context.Result.Error = "invalid_grant";
|
||||||
|
context.Result.IsError = true;
|
||||||
|
context.Result.CustomResponse = customResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,41 +1,22 @@
|
|||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Table;
|
||||||
using Bit.Core.Models.Table;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using IdentityServer4.Models;
|
using IdentityServer4.Models;
|
||||||
using IdentityServer4.Validation;
|
using IdentityServer4.Validation;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using System.Linq;
|
|
||||||
using Bit.Core.Models;
|
|
||||||
using Bit.Core.Identity;
|
using Bit.Core.Identity;
|
||||||
using Bit.Core.Models.Data;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Reflection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.IdentityServer
|
namespace Bit.Core.IdentityServer
|
||||||
{
|
{
|
||||||
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
|
public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwnerPasswordValidationContext>,
|
||||||
|
IResourceOwnerPasswordValidator
|
||||||
{
|
{
|
||||||
private UserManager<User> _userManager;
|
private UserManager<User> _userManager;
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
|
||||||
private readonly IDeviceService _deviceService;
|
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IEventService _eventService;
|
|
||||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
|
||||||
private readonly IMailService _mailService;
|
|
||||||
private readonly ILogger<ResourceOwnerPasswordValidator> _logger;
|
|
||||||
private readonly CurrentContext _currentContext;
|
|
||||||
private readonly GlobalSettings _globalSettings;
|
|
||||||
|
|
||||||
public ResourceOwnerPasswordValidator(
|
public ResourceOwnerPasswordValidator(
|
||||||
UserManager<User> userManager,
|
UserManager<User> userManager,
|
||||||
@ -51,366 +32,55 @@ namespace Bit.Core.IdentityServer
|
|||||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||||
CurrentContext currentContext,
|
CurrentContext currentContext,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings)
|
||||||
|
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||||
|
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
|
||||||
|
applicationCacheService, mailService, logger, currentContext, globalSettings)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_deviceRepository = deviceRepository;
|
|
||||||
_deviceService = deviceService;
|
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_eventService = eventService;
|
|
||||||
_organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
|
|
||||||
_organizationRepository = organizationRepository;
|
|
||||||
_organizationUserRepository = organizationUserRepository;
|
|
||||||
_applicationCacheService = applicationCacheService;
|
|
||||||
_mailService = mailService;
|
|
||||||
_logger = logger;
|
|
||||||
_currentContext = currentContext;
|
|
||||||
_globalSettings = globalSettings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
|
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
|
||||||
{
|
{
|
||||||
var twoFactorToken = context.Request.Raw["TwoFactorToken"]?.ToString();
|
await ValidateAsync(context, context.Request);
|
||||||
var twoFactorProvider = context.Request.Raw["TwoFactorProvider"]?.ToString();
|
}
|
||||||
var twoFactorRemember = context.Request.Raw["TwoFactorRemember"]?.ToString() == "1";
|
|
||||||
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
|
||||||
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
|
||||||
|
|
||||||
|
protected async override Task<(User, bool)> ValidateContextAsync(ResourceOwnerPasswordValidationContext context)
|
||||||
|
{
|
||||||
if (string.IsNullOrWhiteSpace(context.UserName))
|
if (string.IsNullOrWhiteSpace(context.UserName))
|
||||||
{
|
{
|
||||||
await BuildErrorResultAsync(false, context, null);
|
return (null, false);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
|
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
|
||||||
if (user == null || !await _userService.CheckPasswordAsync(user, context.Password))
|
if (user == null || !await _userService.CheckPasswordAsync(user, context.Password))
|
||||||
{
|
{
|
||||||
await BuildErrorResultAsync(false, context, user);
|
return (user, false);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var twoFactorRequirement = await RequiresTwoFactorAsync(user);
|
return (user, true);
|
||||||
if (twoFactorRequirement.Item1)
|
|
||||||
{
|
|
||||||
var twoFactorProviderType = TwoFactorProviderType.Authenticator; // Just defaulting it
|
|
||||||
if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out twoFactorProviderType))
|
|
||||||
{
|
|
||||||
await BuildTwoFactorResultAsync(user, twoFactorRequirement.Item2, context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var verified = await VerifyTwoFactor(user, twoFactorRequirement.Item2,
|
|
||||||
twoFactorProviderType, twoFactorToken);
|
|
||||||
if (!verified && twoFactorProviderType != TwoFactorProviderType.Remember)
|
|
||||||
{
|
|
||||||
await BuildErrorResultAsync(true, context, user);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (!verified && twoFactorProviderType == TwoFactorProviderType.Remember)
|
|
||||||
{
|
|
||||||
await Task.Delay(2000); // Delay for brute force.
|
|
||||||
await BuildTwoFactorResultAsync(user, twoFactorRequirement.Item2, context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
twoFactorRequest = false;
|
|
||||||
twoFactorRemember = false;
|
|
||||||
twoFactorToken = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var device = await SaveDeviceAsync(user, context);
|
|
||||||
await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BuildSuccessResultAsync(User user, ResourceOwnerPasswordValidationContext context,
|
protected override void SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
|
||||||
Device device, bool sendRememberToken)
|
List<Claim> claims, Dictionary<string, object> customResponse)
|
||||||
{
|
{
|
||||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn);
|
|
||||||
|
|
||||||
var claims = new List<Claim>();
|
|
||||||
|
|
||||||
if (device != null)
|
|
||||||
{
|
|
||||||
claims.Add(new Claim("device", device.Identifier));
|
|
||||||
}
|
|
||||||
|
|
||||||
var customResponse = new Dictionary<string, object>();
|
|
||||||
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
|
|
||||||
{
|
|
||||||
customResponse.Add("PrivateKey", user.PrivateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(user.Key))
|
|
||||||
{
|
|
||||||
customResponse.Add("Key", user.Key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sendRememberToken)
|
|
||||||
{
|
|
||||||
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
|
|
||||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));
|
|
||||||
customResponse.Add("TwoFactorToken", token);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Result = new GrantValidationResult(user.Id.ToString(), "Application",
|
context.Result = new GrantValidationResult(user.Id.ToString(), "Application",
|
||||||
identityProvider: "bitwarden",
|
identityProvider: "bitwarden",
|
||||||
claims: claims.Count > 0 ? claims : null,
|
claims: claims.Count > 0 ? claims : null,
|
||||||
customResponse: customResponse);
|
customResponse: customResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BuildTwoFactorResultAsync(User user, Organization organization,
|
protected override void SetTwoFactorResult(ResourceOwnerPasswordValidationContext context,
|
||||||
ResourceOwnerPasswordValidationContext context)
|
Dictionary<string, object> customResponse)
|
||||||
{
|
{
|
||||||
var providerKeys = new List<byte>();
|
|
||||||
var providers = new Dictionary<byte, Dictionary<string, object>>();
|
|
||||||
|
|
||||||
var enabledProviders = new List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>();
|
|
||||||
if (organization?.GetTwoFactorProviders() != null)
|
|
||||||
{
|
|
||||||
enabledProviders.AddRange(organization.GetTwoFactorProviders().Where(
|
|
||||||
p => organization.TwoFactorProviderIsEnabled(p.Key)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.GetTwoFactorProviders() != null)
|
|
||||||
{
|
|
||||||
foreach (var p in user.GetTwoFactorProviders())
|
|
||||||
{
|
|
||||||
if (await _userService.TwoFactorProviderIsEnabledAsync(p.Key, user))
|
|
||||||
{
|
|
||||||
enabledProviders.Add(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enabledProviders.Any())
|
|
||||||
{
|
|
||||||
await BuildErrorResultAsync(false, context, user);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var provider in enabledProviders)
|
|
||||||
{
|
|
||||||
providerKeys.Add((byte)provider.Key);
|
|
||||||
var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value);
|
|
||||||
providers.Add((byte)provider.Key, infoDict);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Two factor required.",
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Two factor required.",
|
||||||
new Dictionary<string, object>
|
customResponse);
|
||||||
{
|
|
||||||
{ "TwoFactorProviders", providers.Keys },
|
|
||||||
{ "TwoFactorProviders2", providers }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
|
|
||||||
{
|
|
||||||
// Send email now if this is their only 2FA method
|
|
||||||
await _userService.SendTwoFactorEmailAsync(user);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BuildErrorResultAsync(bool twoFactorRequest,
|
protected override void SetErrorResult(ResourceOwnerPasswordValidationContext context,
|
||||||
ResourceOwnerPasswordValidationContext context, User user)
|
Dictionary<string, object> customResponse)
|
||||||
{
|
{
|
||||||
if (user != null)
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse);
|
||||||
{
|
|
||||||
await _eventService.LogUserEventAsync(user.Id,
|
|
||||||
twoFactorRequest ? EventType.User_FailedLogIn2fa : EventType.User_FailedLogIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_globalSettings.SelfHosted)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(Constants.BypassFiltersEventId,
|
|
||||||
string.Format("Failed login attempt{0}{1}", twoFactorRequest ? ", 2FA invalid." : ".",
|
|
||||||
$" {_currentContext.IpAddress}"));
|
|
||||||
}
|
|
||||||
await Task.Delay(2000); // Delay for brute force.
|
|
||||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant,
|
|
||||||
customResponse: new Dictionary<string, object>
|
|
||||||
{{
|
|
||||||
"ErrorModel", new ErrorResponseModel(twoFactorRequest ?
|
|
||||||
"Two-step token is invalid. Try again." : "Username or password is incorrect. Try again.")
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user)
|
|
||||||
{
|
|
||||||
var individualRequired = _userManager.SupportsUserTwoFactor &&
|
|
||||||
await _userManager.GetTwoFactorEnabledAsync(user) &&
|
|
||||||
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
|
|
||||||
|
|
||||||
Organization firstEnabledOrg = null;
|
|
||||||
var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))
|
|
||||||
.ToList();
|
|
||||||
if (orgs.Any())
|
|
||||||
{
|
|
||||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
|
||||||
var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id));
|
|
||||||
if (twoFactorOrgs.Any())
|
|
||||||
{
|
|
||||||
var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
|
||||||
firstEnabledOrg = userOrgs.FirstOrDefault(
|
|
||||||
o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Tuple<bool, Organization>(individualRequired || firstEnabledOrg != null, firstEnabledOrg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
|
|
||||||
{
|
|
||||||
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
|
|
||||||
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Device GetDeviceFromRequest(ResourceOwnerPasswordValidationContext context)
|
|
||||||
{
|
|
||||||
var deviceIdentifier = context.Request.Raw["DeviceIdentifier"]?.ToString();
|
|
||||||
var deviceType = context.Request.Raw["DeviceType"]?.ToString();
|
|
||||||
var deviceName = context.Request.Raw["DeviceName"]?.ToString();
|
|
||||||
var devicePushToken = context.Request.Raw["DevicePushToken"]?.ToString();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(deviceIdentifier) || string.IsNullOrWhiteSpace(deviceType) ||
|
|
||||||
string.IsNullOrWhiteSpace(deviceName) || !Enum.TryParse(deviceType, out DeviceType type))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Device
|
|
||||||
{
|
|
||||||
Identifier = deviceIdentifier,
|
|
||||||
Name = deviceName,
|
|
||||||
Type = type,
|
|
||||||
PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type,
|
|
||||||
string token)
|
|
||||||
{
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case TwoFactorProviderType.Authenticator:
|
|
||||||
case TwoFactorProviderType.Email:
|
|
||||||
case TwoFactorProviderType.Duo:
|
|
||||||
case TwoFactorProviderType.YubiKey:
|
|
||||||
case TwoFactorProviderType.U2f:
|
|
||||||
case TwoFactorProviderType.Remember:
|
|
||||||
if (type != TwoFactorProviderType.Remember &&
|
|
||||||
!(await _userService.TwoFactorProviderIsEnabledAsync(type, user)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return await _userManager.VerifyTwoFactorTokenAsync(user,
|
|
||||||
CoreHelpers.CustomProviderName(type), token);
|
|
||||||
case TwoFactorProviderType.OrganizationDuo:
|
|
||||||
if (!organization?.TwoFactorProviderIsEnabled(type) ?? true)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Dictionary<string, object>> BuildTwoFactorParams(Organization organization, User user,
|
|
||||||
TwoFactorProviderType type, TwoFactorProvider provider)
|
|
||||||
{
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case TwoFactorProviderType.Duo:
|
|
||||||
case TwoFactorProviderType.U2f:
|
|
||||||
case TwoFactorProviderType.Email:
|
|
||||||
case TwoFactorProviderType.YubiKey:
|
|
||||||
if (!(await _userService.TwoFactorProviderIsEnabledAsync(type, user)))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
|
|
||||||
CoreHelpers.CustomProviderName(type));
|
|
||||||
if (type == TwoFactorProviderType.Duo)
|
|
||||||
{
|
|
||||||
return new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["Host"] = provider.MetaData["Host"],
|
|
||||||
["Signature"] = token
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (type == TwoFactorProviderType.U2f)
|
|
||||||
{
|
|
||||||
// TODO: Remove "Challenges" in a future update. Deprecated.
|
|
||||||
var tokens = token?.Split('|');
|
|
||||||
return new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["Challenge"] = tokens != null && tokens.Length > 0 ? tokens[0] : null,
|
|
||||||
["Challenges"] = tokens != null && tokens.Length > 1 ? tokens[1] : null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (type == TwoFactorProviderType.Email)
|
|
||||||
{
|
|
||||||
return new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["Email"] = token
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (type == TwoFactorProviderType.YubiKey)
|
|
||||||
{
|
|
||||||
return new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["Nfc"] = (bool)provider.MetaData["Nfc"]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
case TwoFactorProviderType.OrganizationDuo:
|
|
||||||
if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
|
|
||||||
{
|
|
||||||
return new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
["Host"] = provider.MetaData["Host"],
|
|
||||||
["Signature"] = await _organizationDuoWebTokenProvider.GenerateAsync(organization, user)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Device> SaveDeviceAsync(User user, ResourceOwnerPasswordValidationContext context)
|
|
||||||
{
|
|
||||||
var device = GetDeviceFromRequest(context);
|
|
||||||
if (device != null)
|
|
||||||
{
|
|
||||||
var existingDevice = await _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id);
|
|
||||||
if (existingDevice == null)
|
|
||||||
{
|
|
||||||
device.UserId = user.Id;
|
|
||||||
await _deviceService.SaveAsync(device);
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
if (now - user.CreationDate > TimeSpan.FromMinutes(10))
|
|
||||||
{
|
|
||||||
var deviceType = device.Type.GetType().GetMember(device.Type.ToString())
|
|
||||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
|
|
||||||
if (!_globalSettings.DisableEmailNewDevice)
|
|
||||||
{
|
|
||||||
await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,
|
|
||||||
_currentContext.IpAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return device;
|
|
||||||
}
|
|
||||||
|
|
||||||
return existingDevice;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using IdentityServer4.Models;
|
using IdentityServer4;
|
||||||
|
using IdentityServer4.Models;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
@ -28,8 +29,7 @@ namespace Bit.Core.IdentityServer
|
|||||||
string[] scopes = null)
|
string[] scopes = null)
|
||||||
{
|
{
|
||||||
ClientId = id;
|
ClientId = id;
|
||||||
RequireClientSecret = false;
|
AllowedGrantTypes = new[] { GrantType.ResourceOwnerPassword, GrantType.AuthorizationCode };
|
||||||
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword;
|
|
||||||
RefreshTokenExpiration = TokenExpiration.Sliding;
|
RefreshTokenExpiration = TokenExpiration.Sliding;
|
||||||
RefreshTokenUsage = TokenUsage.ReUse;
|
RefreshTokenUsage = TokenUsage.ReUse;
|
||||||
SlidingRefreshTokenLifetime = 86400 * refreshTokenSlidingDays;
|
SlidingRefreshTokenLifetime = 86400 * refreshTokenSlidingDays;
|
||||||
@ -38,6 +38,39 @@ namespace Bit.Core.IdentityServer
|
|||||||
AccessTokenLifetime = 3600 * accessTokenLifetimeHours;
|
AccessTokenLifetime = 3600 * accessTokenLifetimeHours;
|
||||||
AllowOfflineAccess = true;
|
AllowOfflineAccess = true;
|
||||||
|
|
||||||
|
RequireConsent = false;
|
||||||
|
RequirePkce = true;
|
||||||
|
RequireClientSecret = false;
|
||||||
|
if (id == "web")
|
||||||
|
{
|
||||||
|
RedirectUris = new[] { "https://localhost:8080/sso-connector.html" };
|
||||||
|
PostLogoutRedirectUris = new[] { "https://localhost:8080" };
|
||||||
|
AllowedCorsOrigins = new[] { "https://localhost:8080" };
|
||||||
|
}
|
||||||
|
else if (id == "desktop")
|
||||||
|
{
|
||||||
|
RedirectUris = new[] { "bitwarden://sso-callback" };
|
||||||
|
PostLogoutRedirectUris = new[] { "bitwarden-desktop://logged-out" };
|
||||||
|
}
|
||||||
|
else if (id == "connector")
|
||||||
|
{
|
||||||
|
RedirectUris = new[] { "bwdc://sso-callback" };
|
||||||
|
PostLogoutRedirectUris = new[] { "bwdc://logged-out" };
|
||||||
|
}
|
||||||
|
else if (id == "browser")
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
else if (id == "cli")
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
else if (id == "mobile")
|
||||||
|
{
|
||||||
|
RedirectUris = new[] { "bitwarden://sso-callback" };
|
||||||
|
PostLogoutRedirectUris = new[] { "bitwarden://logged-out" };
|
||||||
|
}
|
||||||
|
|
||||||
if (scopes == null)
|
if (scopes == null)
|
||||||
{
|
{
|
||||||
scopes = new string[] { "api" };
|
scopes = new string[] { "api" };
|
||||||
@ -45,5 +78,25 @@ namespace Bit.Core.IdentityServer
|
|||||||
AllowedScopes = scopes;
|
AllowedScopes = scopes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class OidcIdentityClient : Client
|
||||||
|
{
|
||||||
|
public OidcIdentityClient(GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
ClientId = "oidc-identity";
|
||||||
|
RequireClientSecret = true;
|
||||||
|
RequirePkce = true;
|
||||||
|
ClientSecrets = new List<Secret> { new Secret(globalSettings.OidcIdentityClientKey.Sha256()) };
|
||||||
|
AllowedScopes = new string[]
|
||||||
|
{
|
||||||
|
IdentityServerConstants.StandardScopes.OpenId,
|
||||||
|
IdentityServerConstants.StandardScopes.Profile
|
||||||
|
};
|
||||||
|
AllowedGrantTypes = GrantTypes.Code;
|
||||||
|
Enabled = true;
|
||||||
|
RedirectUris = new List<string> { $"{globalSettings.BaseServiceUri.Identity}/signin-oidc" };
|
||||||
|
RequireConsent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Api
|
||||||
|
{
|
||||||
|
public class VerifyPasswordRequestModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[StringLength(300)]
|
||||||
|
public string MasterPasswordHash { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -5,9 +5,6 @@ using Bit.Core.Models.Table;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using IdentityModel;
|
using IdentityModel;
|
||||||
using IdentityServer4.Services;
|
|
||||||
using IdentityServer4.Stores;
|
|
||||||
using IdentityServer4.Validation;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
@ -339,67 +336,6 @@ namespace Bit.Core.Utilities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IIdentityServerBuilder AddCustomIdentityServerServices(
|
|
||||||
this IServiceCollection services, IWebHostEnvironment env, GlobalSettings globalSettings)
|
|
||||||
{
|
|
||||||
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
|
|
||||||
var identityServerBuilder = services
|
|
||||||
.AddIdentityServer(options =>
|
|
||||||
{
|
|
||||||
options.Endpoints.EnableAuthorizeEndpoint = false;
|
|
||||||
options.Endpoints.EnableIntrospectionEndpoint = false;
|
|
||||||
options.Endpoints.EnableEndSessionEndpoint = false;
|
|
||||||
options.Endpoints.EnableUserInfoEndpoint = false;
|
|
||||||
options.Endpoints.EnableCheckSessionEndpoint = false;
|
|
||||||
options.Endpoints.EnableTokenRevocationEndpoint = false;
|
|
||||||
options.IssuerUri = $"{issuerUri.Scheme}://{issuerUri.Host}";
|
|
||||||
options.Caching.ClientStoreExpiration = new TimeSpan(0, 5, 0);
|
|
||||||
})
|
|
||||||
.AddInMemoryCaching()
|
|
||||||
.AddInMemoryApiResources(ApiResources.GetApiResources())
|
|
||||||
.AddClientStoreCache<ClientStore>();
|
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
|
||||||
{
|
|
||||||
identityServerBuilder.AddDeveloperSigningCredential(false);
|
|
||||||
}
|
|
||||||
else if (globalSettings.SelfHosted &&
|
|
||||||
CoreHelpers.SettingHasValue(globalSettings.IdentityServer.CertificatePassword)
|
|
||||||
&& File.Exists("identity.pfx"))
|
|
||||||
{
|
|
||||||
var identityServerCert = CoreHelpers.GetCertificate("identity.pfx",
|
|
||||||
globalSettings.IdentityServer.CertificatePassword);
|
|
||||||
identityServerBuilder.AddSigningCredential(identityServerCert);
|
|
||||||
}
|
|
||||||
else if (CoreHelpers.SettingHasValue(globalSettings.IdentityServer.CertificateThumbprint))
|
|
||||||
{
|
|
||||||
var identityServerCert = CoreHelpers.GetCertificate(
|
|
||||||
globalSettings.IdentityServer.CertificateThumbprint);
|
|
||||||
identityServerBuilder.AddSigningCredential(identityServerCert);
|
|
||||||
}
|
|
||||||
else if (!globalSettings.SelfHosted &&
|
|
||||||
CoreHelpers.SettingHasValue(globalSettings.Storage?.ConnectionString) &&
|
|
||||||
CoreHelpers.SettingHasValue(globalSettings.IdentityServer.CertificatePassword))
|
|
||||||
{
|
|
||||||
var storageAccount = CloudStorageAccount.Parse(globalSettings.Storage.ConnectionString);
|
|
||||||
var identityServerCert = CoreHelpers.GetBlobCertificateAsync(storageAccount, "certificates",
|
|
||||||
"identity.pfx", globalSettings.IdentityServer.CertificatePassword).GetAwaiter().GetResult();
|
|
||||||
identityServerBuilder.AddSigningCredential(identityServerCert);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new Exception("No identity certificate to use.");
|
|
||||||
}
|
|
||||||
|
|
||||||
services.AddTransient<ClientStore>();
|
|
||||||
services.AddTransient<ICorsPolicyService, CustomCorsPolicyService>();
|
|
||||||
services.AddScoped<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
|
|
||||||
services.AddScoped<IProfileService, ProfileService>();
|
|
||||||
services.AddSingleton<IPersistedGrantStore, PersistedGrantStore>();
|
|
||||||
|
|
||||||
return identityServerBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void AddCustomDataProtectionServices(
|
public static void AddCustomDataProtectionServices(
|
||||||
this IServiceCollection services, IWebHostEnvironment env, GlobalSettings globalSettings)
|
this IServiceCollection services, IWebHostEnvironment env, GlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
@ -435,6 +371,43 @@ namespace Bit.Core.Utilities
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IIdentityServerBuilder AddIdentityServerCertificate(
|
||||||
|
this IIdentityServerBuilder identityServerBuilder, IWebHostEnvironment env, GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
identityServerBuilder.AddDeveloperSigningCredential(false);
|
||||||
|
}
|
||||||
|
else if (globalSettings.SelfHosted &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.IdentityServer.CertificatePassword)
|
||||||
|
&& File.Exists("identity.pfx"))
|
||||||
|
{
|
||||||
|
var identityServerCert = CoreHelpers.GetCertificate("identity.pfx",
|
||||||
|
globalSettings.IdentityServer.CertificatePassword);
|
||||||
|
identityServerBuilder.AddSigningCredential(identityServerCert);
|
||||||
|
}
|
||||||
|
else if (CoreHelpers.SettingHasValue(globalSettings.IdentityServer.CertificateThumbprint))
|
||||||
|
{
|
||||||
|
var identityServerCert = CoreHelpers.GetCertificate(
|
||||||
|
globalSettings.IdentityServer.CertificateThumbprint);
|
||||||
|
identityServerBuilder.AddSigningCredential(identityServerCert);
|
||||||
|
}
|
||||||
|
else if (!globalSettings.SelfHosted &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.Storage?.ConnectionString) &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.IdentityServer.CertificatePassword))
|
||||||
|
{
|
||||||
|
var storageAccount = CloudStorageAccount.Parse(globalSettings.Storage.ConnectionString);
|
||||||
|
var identityServerCert = CoreHelpers.GetBlobCertificateAsync(storageAccount, "certificates",
|
||||||
|
"identity.pfx", globalSettings.IdentityServer.CertificatePassword).GetAwaiter().GetResult();
|
||||||
|
identityServerBuilder.AddSigningCredential(identityServerCert);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("No identity certificate to use.");
|
||||||
|
}
|
||||||
|
return identityServerBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
public static GlobalSettings AddGlobalSettingsServices(this IServiceCollection services,
|
public static GlobalSettings AddGlobalSettingsServices(this IServiceCollection services,
|
||||||
IConfiguration configuration)
|
IConfiguration configuration)
|
||||||
{
|
{
|
||||||
|
@ -6,11 +6,13 @@
|
|||||||
"identity": "https://identity.bitwarden.com",
|
"identity": "https://identity.bitwarden.com",
|
||||||
"admin": "https://admin.bitwarden.com",
|
"admin": "https://admin.bitwarden.com",
|
||||||
"notifications": "https://notifications.bitwarden.com",
|
"notifications": "https://notifications.bitwarden.com",
|
||||||
|
"sso": "https://sso.bitwarden.com",
|
||||||
"internalNotifications": "https://notifications.bitwarden.com",
|
"internalNotifications": "https://notifications.bitwarden.com",
|
||||||
"internalAdmin": "https://admin.bitwarden.com",
|
"internalAdmin": "https://admin.bitwarden.com",
|
||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com"
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
|
"internalSso": "https://sso.bitwarden.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,13 @@
|
|||||||
"identity": "http://localhost:33656",
|
"identity": "http://localhost:33656",
|
||||||
"admin": "http://localhost:62911",
|
"admin": "http://localhost:62911",
|
||||||
"notifications": "http://localhost:61840",
|
"notifications": "http://localhost:61840",
|
||||||
|
"sso": "http://localhost:51822",
|
||||||
"internalNotifications": "http://localhost:61840",
|
"internalNotifications": "http://localhost:61840",
|
||||||
"internalAdmin": "http://localhost:62911",
|
"internalAdmin": "http://localhost:62911",
|
||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "http://localhost:4001"
|
"internalVault": "http://localhost:4001",
|
||||||
|
"internalSso": "http://localhost:51822"
|
||||||
},
|
},
|
||||||
"sqlServer": {
|
"sqlServer": {
|
||||||
"connectionString": "SECRET"
|
"connectionString": "SECRET"
|
||||||
|
227
src/Identity/Controllers/AccountController.cs
Normal file
227
src/Identity/Controllers/AccountController.cs
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Models.Table;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Identity.Models;
|
||||||
|
using IdentityModel;
|
||||||
|
using IdentityServer4.Services;
|
||||||
|
using IdentityServer4.Stores;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.Identity.Controllers
|
||||||
|
{
|
||||||
|
public class AccountController : Controller
|
||||||
|
{
|
||||||
|
private readonly IIdentityServerInteractionService _interaction;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly IClientStore _clientStore;
|
||||||
|
private readonly ILogger<AccountController> _logger;
|
||||||
|
|
||||||
|
public AccountController(
|
||||||
|
IIdentityServerInteractionService interaction,
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IClientStore clientStore,
|
||||||
|
ILogger<AccountController> logger)
|
||||||
|
{
|
||||||
|
_interaction = interaction;
|
||||||
|
_userRepository = userRepository;
|
||||||
|
_clientStore = clientStore;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Login(string returnUrl)
|
||||||
|
{
|
||||||
|
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||||
|
if (context.Parameters.AllKeys.Contains("domain_hint") &&
|
||||||
|
!string.IsNullOrWhiteSpace(context.Parameters["domain_hint"]))
|
||||||
|
{
|
||||||
|
return RedirectToAction(nameof(ExternalChallenge),
|
||||||
|
new { organizationIdentifier = context.Parameters["domain_hint"], returnUrl = returnUrl });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("No domain_hint provided.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult ExternalChallenge(string organizationIdentifier, string returnUrl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(organizationIdentifier))
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid organization reference id.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Lookup sso config and create a domain hint
|
||||||
|
var domainHint = "oidc_okta";
|
||||||
|
// Temp hardcoded orgs
|
||||||
|
if (organizationIdentifier == "org_oidc_okta")
|
||||||
|
{
|
||||||
|
domainHint = "oidc_okta";
|
||||||
|
}
|
||||||
|
else if (organizationIdentifier == "org_oidc_onelogin")
|
||||||
|
{
|
||||||
|
domainHint = "oidc_onelogin";
|
||||||
|
}
|
||||||
|
else if (organizationIdentifier == "org_saml2_onelogin")
|
||||||
|
{
|
||||||
|
domainHint = "saml2_onelogin";
|
||||||
|
}
|
||||||
|
else if (organizationIdentifier == "org_saml2_sustainsys")
|
||||||
|
{
|
||||||
|
domainHint = "saml2_sustainsys";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Organization not found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider = "sso";
|
||||||
|
var props = new AuthenticationProperties
|
||||||
|
{
|
||||||
|
RedirectUri = Url.Action(nameof(ExternalCallback)),
|
||||||
|
Items =
|
||||||
|
{
|
||||||
|
{ "return_url", returnUrl },
|
||||||
|
{ "domain_hint", domainHint },
|
||||||
|
{ "scheme", provider },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return Challenge(props, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult> ExternalCallback()
|
||||||
|
{
|
||||||
|
// Read external identity from the temporary cookie
|
||||||
|
var result = await HttpContext.AuthenticateAsync(
|
||||||
|
IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
|
||||||
|
if (result?.Succeeded != true)
|
||||||
|
{
|
||||||
|
throw new Exception("External authentication error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debugging
|
||||||
|
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
|
||||||
|
_logger.LogDebug("External claims: {@claims}", externalClaims);
|
||||||
|
|
||||||
|
var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
// Should never happen
|
||||||
|
throw new Exception("Cannot find user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// this allows us to collect any additonal claims or properties
|
||||||
|
// for the specific prtotocols used and store them in the local auth cookie.
|
||||||
|
// this is typically used to store data needed for signout from those protocols.
|
||||||
|
var additionalLocalClaims = new List<Claim>();
|
||||||
|
var localSignInProps = new AuthenticationProperties
|
||||||
|
{
|
||||||
|
IsPersistent = true,
|
||||||
|
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1)
|
||||||
|
};
|
||||||
|
ProcessLoginCallbackForOidc(result, additionalLocalClaims, localSignInProps);
|
||||||
|
|
||||||
|
// issue authentication cookie for user
|
||||||
|
await HttpContext.SignInAsync(user.Id.ToString(), user.Email, provider,
|
||||||
|
localSignInProps, additionalLocalClaims.ToArray());
|
||||||
|
|
||||||
|
// delete temporary cookie used during external authentication
|
||||||
|
await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
|
||||||
|
|
||||||
|
// retrieve return URL
|
||||||
|
var returnUrl = result.Properties.Items["return_url"] ?? "~/";
|
||||||
|
|
||||||
|
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||||
|
if (context != null)
|
||||||
|
{
|
||||||
|
if (await IsPkceClientAsync(context.ClientId))
|
||||||
|
{
|
||||||
|
// if the client is PKCE then we assume it's native, so this change in how to
|
||||||
|
// return the response is for better UX for the end user.
|
||||||
|
return View("Redirect", new RedirectViewModel { RedirectUrl = returnUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
|
||||||
|
return Redirect(returnUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// request for a local page
|
||||||
|
if (Url.IsLocalUrl(returnUrl))
|
||||||
|
{
|
||||||
|
return Redirect(returnUrl);
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrEmpty(returnUrl))
|
||||||
|
{
|
||||||
|
return Redirect("~/");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// user might have clicked on a malicious link - should be logged
|
||||||
|
throw new Exception("invalid return URL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims)>
|
||||||
|
FindUserFromExternalProviderAsync(AuthenticateResult result)
|
||||||
|
{
|
||||||
|
var externalUser = result.Principal;
|
||||||
|
|
||||||
|
// try to determine the unique id of the external user (issued by the provider)
|
||||||
|
// the most common claim type for that are the sub claim and the NameIdentifier
|
||||||
|
// depending on the external provider, some other claim type might be used
|
||||||
|
var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??
|
||||||
|
externalUser.FindFirst(ClaimTypes.NameIdentifier) ??
|
||||||
|
throw new Exception("Unknown userid");
|
||||||
|
|
||||||
|
// remove the user id claim so we don't include it as an extra claim if/when we provision the user
|
||||||
|
var claims = externalUser.Claims.ToList();
|
||||||
|
claims.Remove(userIdClaim);
|
||||||
|
|
||||||
|
var provider = result.Properties.Items["scheme"];
|
||||||
|
var providerUserId = userIdClaim.Value;
|
||||||
|
var user = await _userRepository.GetByIdAsync(new Guid(providerUserId));
|
||||||
|
|
||||||
|
return (user, provider, providerUserId, claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessLoginCallbackForOidc(AuthenticateResult externalResult,
|
||||||
|
List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
||||||
|
{
|
||||||
|
// if the external system sent a session id claim, copy it over
|
||||||
|
// so we can use it for single sign-out
|
||||||
|
var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
|
||||||
|
if (sid != null)
|
||||||
|
{
|
||||||
|
localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the external provider issued an id_token, we'll keep it for signout
|
||||||
|
var id_token = externalResult.Properties.GetTokenValue("id_token");
|
||||||
|
if (id_token != null)
|
||||||
|
{
|
||||||
|
localSignInProps.StoreTokens(
|
||||||
|
new[] { new AuthenticationToken { Name = "id_token", Value = id_token } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsPkceClientAsync(string client_id)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(client_id))
|
||||||
|
{
|
||||||
|
var client = await _clientStore.FindEnabledClientByIdAsync(client_id);
|
||||||
|
return client?.RequirePkce == true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
src/Identity/Models/RedirectViewModel.cs
Normal file
7
src/Identity/Models/RedirectViewModel.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Identity.Models
|
||||||
|
{
|
||||||
|
public class RedirectViewModel
|
||||||
|
{
|
||||||
|
public string RedirectUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,12 @@ using AspNetCoreRateLimit;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.IdentityModel.Logging;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using IdentityServer4.Stores;
|
||||||
|
using Bit.Core.IdentityServer;
|
||||||
|
using IdentityServer4.Services;
|
||||||
|
|
||||||
namespace Bit.Identity
|
namespace Bit.Identity
|
||||||
{
|
{
|
||||||
@ -49,6 +55,9 @@ namespace Bit.Identity
|
|||||||
// Caching
|
// Caching
|
||||||
services.AddMemoryCache();
|
services.AddMemoryCache();
|
||||||
|
|
||||||
|
// Mvc
|
||||||
|
services.AddMvc();
|
||||||
|
|
||||||
if (!globalSettings.SelfHosted)
|
if (!globalSettings.SelfHosted)
|
||||||
{
|
{
|
||||||
// Rate limiting
|
// Rate limiting
|
||||||
@ -56,8 +65,35 @@ namespace Bit.Identity
|
|||||||
services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
|
services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
|
||||||
|
|
||||||
|
// Authentication
|
||||||
|
services
|
||||||
|
.AddAuthentication()
|
||||||
|
.AddOpenIdConnect("sso", "Single Sign On", options =>
|
||||||
|
{
|
||||||
|
options.Authority = globalSettings.BaseServiceUri.InternalSso;
|
||||||
|
options.RequireHttpsMetadata = !Environment.IsDevelopment() &&
|
||||||
|
globalSettings.BaseServiceUri.InternalIdentity.StartsWith("https");
|
||||||
|
options.ClientId = "oidc-identity";
|
||||||
|
options.ClientSecret = globalSettings.OidcIdentityClientKey;
|
||||||
|
|
||||||
|
options.SignInScheme = IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme;
|
||||||
|
options.ResponseType = "code";
|
||||||
|
|
||||||
|
options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents
|
||||||
|
{
|
||||||
|
OnRedirectToIdentityProvider = context =>
|
||||||
|
{
|
||||||
|
// Pass domain_hint onto the sso idp
|
||||||
|
context.ProtocolMessage.DomainHint = context.Properties.Items["domain_hint"];
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// IdentityServer
|
// IdentityServer
|
||||||
services.AddCustomIdentityServerServices(Environment, globalSettings);
|
AddCustomIdentityServerServices(services, Environment, globalSettings);
|
||||||
|
|
||||||
// Identity
|
// Identity
|
||||||
services.AddCustomIdentityServices(globalSettings);
|
services.AddCustomIdentityServices(globalSettings);
|
||||||
@ -80,6 +116,8 @@ namespace Bit.Identity
|
|||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ILogger<Startup> logger)
|
ILogger<Startup> logger)
|
||||||
{
|
{
|
||||||
|
IdentityModelEventSource.ShowPII = true;
|
||||||
|
|
||||||
app.UseSerilog(env, appLifetime, globalSettings);
|
app.UseSerilog(env, appLifetime, globalSettings);
|
||||||
|
|
||||||
// Default Middleware
|
// Default Middleware
|
||||||
@ -95,14 +133,58 @@ namespace Bit.Identity
|
|||||||
app.UseForwardedHeaders(globalSettings);
|
app.UseForwardedHeaders(globalSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add static files to the request pipeline.
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
// Add routing
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
// Add Cors
|
||||||
|
app.UseCors(policy => policy.SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings))
|
||||||
|
.AllowAnyMethod().AllowAnyHeader().AllowCredentials());
|
||||||
|
|
||||||
// Add current context
|
// Add current context
|
||||||
app.UseMiddleware<CurrentContextMiddleware>();
|
app.UseMiddleware<CurrentContextMiddleware>();
|
||||||
|
|
||||||
// Add IdentityServer to the request pipeline.
|
// Add IdentityServer to the request pipeline.
|
||||||
app.UseIdentityServer();
|
app.UseIdentityServer();
|
||||||
|
|
||||||
|
// Add Mvc stuff
|
||||||
|
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
|
||||||
|
|
||||||
// Log startup
|
// Log startup
|
||||||
logger.LogInformation(Constants.BypassFiltersEventId, globalSettings.ProjectName + " started.");
|
logger.LogInformation(Constants.BypassFiltersEventId, globalSettings.ProjectName + " started.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IIdentityServerBuilder AddCustomIdentityServerServices(IServiceCollection services,
|
||||||
|
IWebHostEnvironment env, GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
|
||||||
|
|
||||||
|
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
|
||||||
|
var identityServerBuilder = services
|
||||||
|
.AddIdentityServer(options =>
|
||||||
|
{
|
||||||
|
options.Endpoints.EnableIntrospectionEndpoint = false;
|
||||||
|
options.Endpoints.EnableEndSessionEndpoint = false;
|
||||||
|
options.Endpoints.EnableUserInfoEndpoint = false;
|
||||||
|
options.Endpoints.EnableCheckSessionEndpoint = false;
|
||||||
|
options.Endpoints.EnableTokenRevocationEndpoint = false;
|
||||||
|
options.IssuerUri = $"{issuerUri.Scheme}://{issuerUri.Host}";
|
||||||
|
options.Caching.ClientStoreExpiration = new TimeSpan(0, 5, 0);
|
||||||
|
})
|
||||||
|
.AddInMemoryCaching()
|
||||||
|
.AddInMemoryApiResources(ApiResources.GetApiResources())
|
||||||
|
.AddClientStoreCache<ClientStore>()
|
||||||
|
.AddCustomTokenRequestValidator<CustomTokenRequestValidator>()
|
||||||
|
.AddProfileService<ProfileService>()
|
||||||
|
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
|
||||||
|
.AddPersistedGrantStore<PersistedGrantStore>()
|
||||||
|
.AddClientStore<ClientStore>()
|
||||||
|
.AddIdentityServerCertificate(env, globalSettings);
|
||||||
|
|
||||||
|
services.AddTransient<ICorsPolicyService, CustomCorsPolicyService>();
|
||||||
|
return identityServerBuilder;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
src/Identity/Views/Shared/Redirect.cshtml
Normal file
12
src/Identity/Views/Shared/Redirect.cshtml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@model Bit.Identity.Models.RedirectViewModel
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0;url=@Model.RedirectUrl" data-url="@Model.RedirectUrl">
|
||||||
|
<script>
|
||||||
|
window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url");
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>You are now being returned to the application. Once complete, you may close this tab.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -6,11 +6,13 @@
|
|||||||
"identity": "https://identity.bitwarden.com",
|
"identity": "https://identity.bitwarden.com",
|
||||||
"admin": "https://admin.bitwarden.com",
|
"admin": "https://admin.bitwarden.com",
|
||||||
"notifications": "https://notifications.bitwarden.com",
|
"notifications": "https://notifications.bitwarden.com",
|
||||||
|
"sso": "https://sso.bitwarden.com",
|
||||||
"internalNotifications": "https://notifications.bitwarden.com",
|
"internalNotifications": "https://notifications.bitwarden.com",
|
||||||
"internalAdmin": "https://admin.bitwarden.com",
|
"internalAdmin": "https://admin.bitwarden.com",
|
||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com"
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
|
"internalSso": "https://sso.bitwarden.com"
|
||||||
},
|
},
|
||||||
"braintree": {
|
"braintree": {
|
||||||
"production": true
|
"production": true
|
||||||
|
@ -4,17 +4,20 @@
|
|||||||
"siteName": "Bitwarden",
|
"siteName": "Bitwarden",
|
||||||
"projectName": "Identity",
|
"projectName": "Identity",
|
||||||
"stripeApiKey": "SECRET",
|
"stripeApiKey": "SECRET",
|
||||||
|
"oidcIdentityClientKey": "SECRET",
|
||||||
"baseServiceUri": {
|
"baseServiceUri": {
|
||||||
"vault": "https://localhost:8080",
|
"vault": "https://localhost:8080",
|
||||||
"api": "http://localhost:4000",
|
"api": "http://localhost:4000",
|
||||||
"identity": "http://localhost:33656",
|
"identity": "http://localhost:33656",
|
||||||
"admin": "http://localhost:62911",
|
"admin": "http://localhost:62911",
|
||||||
"notifications": "http://localhost:61840",
|
"notifications": "http://localhost:61840",
|
||||||
|
"sso": "http://localhost:51822",
|
||||||
"internalNotifications": "http://localhost:61840",
|
"internalNotifications": "http://localhost:61840",
|
||||||
"internalAdmin": "http://localhost:62911",
|
"internalAdmin": "http://localhost:62911",
|
||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "http://localhost:4001"
|
"internalVault": "http://localhost:4001",
|
||||||
|
"internalSso": "http://localhost:51822"
|
||||||
},
|
},
|
||||||
"sqlServer": {
|
"sqlServer": {
|
||||||
"connectionString": "SECRET"
|
"connectionString": "SECRET"
|
||||||
|
@ -6,11 +6,13 @@
|
|||||||
"identity": "https://identity.bitwarden.com",
|
"identity": "https://identity.bitwarden.com",
|
||||||
"admin": "https://admin.bitwarden.com",
|
"admin": "https://admin.bitwarden.com",
|
||||||
"notifications": "https://notifications.bitwarden.com",
|
"notifications": "https://notifications.bitwarden.com",
|
||||||
|
"sso": "https://sso.bitwarden.com",
|
||||||
"internalNotifications": "https://notifications.bitwarden.com",
|
"internalNotifications": "https://notifications.bitwarden.com",
|
||||||
"internalAdmin": "https://admin.bitwarden.com",
|
"internalAdmin": "https://admin.bitwarden.com",
|
||||||
"internalIdentity": "https://identity.bitwarden.com",
|
"internalIdentity": "https://identity.bitwarden.com",
|
||||||
"internalApi": "https://api.bitwarden.com",
|
"internalApi": "https://api.bitwarden.com",
|
||||||
"internalVault": "https://vault.bitwarden.com"
|
"internalVault": "https://vault.bitwarden.com",
|
||||||
|
"internalSso": "https://sso.bitwarden.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,13 @@
|
|||||||
"identity": "http://localhost:33656",
|
"identity": "http://localhost:33656",
|
||||||
"admin": "http://localhost:62911",
|
"admin": "http://localhost:62911",
|
||||||
"notifications": "http://localhost:61840",
|
"notifications": "http://localhost:61840",
|
||||||
|
"sso": "http://localhost:51822",
|
||||||
"internalNotifications": "http://localhost:61840",
|
"internalNotifications": "http://localhost:61840",
|
||||||
"internalAdmin": "http://localhost:62911",
|
"internalAdmin": "http://localhost:62911",
|
||||||
"internalIdentity": "http://localhost:33656",
|
"internalIdentity": "http://localhost:33656",
|
||||||
"internalApi": "http://localhost:4000",
|
"internalApi": "http://localhost:4000",
|
||||||
"internalVault": "http://localhost:4001"
|
"internalVault": "http://localhost:4001",
|
||||||
|
"internalSso": "http://localhost:51822"
|
||||||
},
|
},
|
||||||
"sqlServer": {
|
"sqlServer": {
|
||||||
"connectionString": "SECRET"
|
"connectionString": "SECRET"
|
||||||
|
@ -26,12 +26,14 @@ namespace Bit.Setup
|
|||||||
["globalSettings__baseServiceUri__api"] = "http://localhost/api",
|
["globalSettings__baseServiceUri__api"] = "http://localhost/api",
|
||||||
["globalSettings__baseServiceUri__identity"] = "http://localhost/identity",
|
["globalSettings__baseServiceUri__identity"] = "http://localhost/identity",
|
||||||
["globalSettings__baseServiceUri__admin"] = "http://localhost/admin",
|
["globalSettings__baseServiceUri__admin"] = "http://localhost/admin",
|
||||||
|
["globalSettings__baseServiceUri__sso"] = "http://localhost/sso",
|
||||||
["globalSettings__baseServiceUri__notifications"] = "http://localhost/notifications",
|
["globalSettings__baseServiceUri__notifications"] = "http://localhost/notifications",
|
||||||
["globalSettings__baseServiceUri__internalNotifications"] = "http://notifications:5000",
|
["globalSettings__baseServiceUri__internalNotifications"] = "http://notifications:5000",
|
||||||
["globalSettings__baseServiceUri__internalAdmin"] = "http://admin:5000",
|
["globalSettings__baseServiceUri__internalAdmin"] = "http://admin:5000",
|
||||||
["globalSettings__baseServiceUri__internalIdentity"] = "http://identity:5000",
|
["globalSettings__baseServiceUri__internalIdentity"] = "http://identity:5000",
|
||||||
["globalSettings__baseServiceUri__internalApi"] = "http://api:5000",
|
["globalSettings__baseServiceUri__internalApi"] = "http://api:5000",
|
||||||
["globalSettings__baseServiceUri__internalVault"] = "http://web:5000",
|
["globalSettings__baseServiceUri__internalVault"] = "http://web:5000",
|
||||||
|
["globalSettings__baseServiceUri__internalSso"] = "http://sso:5000",
|
||||||
["globalSettings__pushRelayBaseUri"] = "https://push.bitwarden.com",
|
["globalSettings__pushRelayBaseUri"] = "https://push.bitwarden.com",
|
||||||
["globalSettings__installation__identityUri"] = "https://identity.bitwarden.com",
|
["globalSettings__installation__identityUri"] = "https://identity.bitwarden.com",
|
||||||
};
|
};
|
||||||
@ -89,6 +91,7 @@ namespace Bit.Setup
|
|||||||
["globalSettings__baseServiceUri__identity"] = $"{_context.Config.Url}/identity",
|
["globalSettings__baseServiceUri__identity"] = $"{_context.Config.Url}/identity",
|
||||||
["globalSettings__baseServiceUri__admin"] = $"{_context.Config.Url}/admin",
|
["globalSettings__baseServiceUri__admin"] = $"{_context.Config.Url}/admin",
|
||||||
["globalSettings__baseServiceUri__notifications"] = $"{_context.Config.Url}/notifications",
|
["globalSettings__baseServiceUri__notifications"] = $"{_context.Config.Url}/notifications",
|
||||||
|
["globalSettings__baseServiceUri__sso"] = $"{_context.Config.Url}/sso",
|
||||||
["globalSettings__sqlServer__connectionString"] = $"\"{dbConnectionString}\"",
|
["globalSettings__sqlServer__connectionString"] = $"\"{dbConnectionString}\"",
|
||||||
["globalSettings__identityServer__certificatePassword"] = _context.Install?.IdentityCertPassword,
|
["globalSettings__identityServer__certificatePassword"] = _context.Install?.IdentityCertPassword,
|
||||||
["globalSettings__attachment__baseDirectory"] = $"{_context.OutputDir}/core/attachments",
|
["globalSettings__attachment__baseDirectory"] = $"{_context.OutputDir}/core/attachments",
|
||||||
@ -100,6 +103,8 @@ namespace Bit.Setup
|
|||||||
["globalSettings__licenseDirectory"] = $"{_context.OutputDir}/core/licenses",
|
["globalSettings__licenseDirectory"] = $"{_context.OutputDir}/core/licenses",
|
||||||
["globalSettings__internalIdentityKey"] = _context.Stub ? "RANDOM_IDENTITY_KEY" :
|
["globalSettings__internalIdentityKey"] = _context.Stub ? "RANDOM_IDENTITY_KEY" :
|
||||||
Helpers.SecureRandomString(64, alpha: true, numeric: true),
|
Helpers.SecureRandomString(64, alpha: true, numeric: true),
|
||||||
|
["globalSettings__oidcIdentityClientKey"] = _context.Stub ? "RANDOM_IDENTITY_KEY" :
|
||||||
|
Helpers.SecureRandomString(64, alpha: true, numeric: true),
|
||||||
["globalSettings__duo__aKey"] = _context.Stub ? "RANDOM_DUO_AKEY" :
|
["globalSettings__duo__aKey"] = _context.Stub ? "RANDOM_DUO_AKEY" :
|
||||||
Helpers.SecureRandomString(64, alpha: true, numeric: true),
|
Helpers.SecureRandomString(64, alpha: true, numeric: true),
|
||||||
["globalSettings__installation__id"] = _context.Install?.InstallationId.ToString(),
|
["globalSettings__installation__id"] = _context.Install?.InstallationId.ToString(),
|
||||||
|
@ -87,6 +87,23 @@ services:
|
|||||||
- default
|
- default
|
||||||
- public
|
- public
|
||||||
|
|
||||||
|
sso:
|
||||||
|
image: bitwarden/sso:{{{CoreVersion}}}
|
||||||
|
container_name: bitwarden-sso
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ../identity:/etc/bitwarden/identity
|
||||||
|
- ../core:/etc/bitwarden/core
|
||||||
|
- ../ca-certificates:/etc/bitwarden/ca-certificates
|
||||||
|
- ../logs/sso:/etc/bitwarden/logs
|
||||||
|
env_file:
|
||||||
|
- global.env
|
||||||
|
- ../env/uid.env
|
||||||
|
- ../env/global.override.env
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- public
|
||||||
|
|
||||||
admin:
|
admin:
|
||||||
image: bitwarden/admin:{{{CoreVersion}}}
|
image: bitwarden/admin:{{{CoreVersion}}}
|
||||||
container_name: bitwarden-admin
|
container_name: bitwarden-admin
|
||||||
|
@ -104,6 +104,10 @@ server {
|
|||||||
proxy_pass http://identity:5000/;
|
proxy_pass http://identity:5000/;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /sso/ {
|
||||||
|
proxy_pass http://sso:5000/;
|
||||||
|
}
|
||||||
|
|
||||||
location /icons/ {
|
location /icons/ {
|
||||||
proxy_pass http://icons:5000/;
|
proxy_pass http://icons:5000/;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user