diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index b1978c292..0bc9da2e1 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -110,6 +110,7 @@ namespace Bit.Api // Services services.AddBaseServices(); services.AddDefaultServices(globalSettings); + services.AddCoreLocalizationServices(); // MVC services.AddMvc(config => @@ -162,6 +163,9 @@ namespace Bit.Api app.UseForwardedHeaders(globalSettings); } + // Add localization + app.UseCoreLocalization(); + // Add static files to the request pipeline. app.UseStaticFiles(); diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index dd12b0726..e438bb494 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -60,4 +60,8 @@ + + + + diff --git a/src/Core/Properties/AssemblyInfo.cs b/src/Core/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..4c45d9af2 --- /dev/null +++ b/src/Core/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; +using Microsoft.Extensions.Localization; + +[assembly: ResourceLocation("Resources")] +[assembly: RootNamespace("Bit.Core")] diff --git a/src/Core/Repositories/SqlServer/OrganizationRepository.cs b/src/Core/Repositories/SqlServer/OrganizationRepository.cs index c1433ab96..83dd3ef31 100644 --- a/src/Core/Repositories/SqlServer/OrganizationRepository.cs +++ b/src/Core/Repositories/SqlServer/OrganizationRepository.cs @@ -26,6 +26,7 @@ namespace Bit.Core.Repositories.SqlServer { var results = await connection.QueryAsync( "[dbo].[Organization_ReadByIdentifier]", + new { Identifier = identifier }, commandType: CommandType.StoredProcedure); return results.SingleOrDefault(); diff --git a/src/Core/Resources/SharedResources.cs b/src/Core/Resources/SharedResources.cs new file mode 100644 index 000000000..64801700d --- /dev/null +++ b/src/Core/Resources/SharedResources.cs @@ -0,0 +1,8 @@ +using System; + +namespace Bit.Core.Resources +{ + public class SharedResources + { + } +} diff --git a/src/Core/Resources/SharedResources.en.resx b/src/Core/Resources/SharedResources.en.resx new file mode 100644 index 000000000..58f26f2e5 --- /dev/null +++ b/src/Core/Resources/SharedResources.en.resx @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Home + Home page + + + Policies + + + Enabled + + + Two-step Login + + + Require users to set up two-step login on their personal accounts. + + + Master Password + + + Set minimum requirements for master password strength. + + + Password Generator + + + Set minimum requirements for password generator configuration. + + + Edit Policy - {0} + + + Organization members who do not have two-step login enabled for their personal account will be removed from the organization and will receive an email notifying them about the change. + + + Save + + + Cancel + + + Minimum Complexity Score + + + Minimum Length + + + Weak + + + Good + + + Strong + + + Default Type + + + User Preference + + + Password + + + Passphrase + + + Minimum Special + + + Minimum Numbers + + + Minimum Number of Words + + + Capitalize + + + Include Number + + + Warning + + + A-Z + + + a-z + + + 0-9 + + + !@#$%^&* + + + Select + + + The field {0} must be greater than or equal to {1}. + + + Single Sign-On + + + Edit SSO Configuration + + + Type + + + OpenID Connect + + + SAML 2.0 + + + SSO Configuration + + + OpenID Connect Configuration + + + Authority + + + Client ID + + + Client Secret + + + Callback Path + + + Signed Out Callback Path + + + SAML Service Provider Configuration + + + Entity ID + + + SP Entity ID + + + Name ID Format + + + Not Configured + + + Unspecified + + + Email Address + + + X.509 Subject Name + + + Windows Domain Qualified Name + + + Kerberos Principal Name + + + Entity Identifier + + + Persistent + + + Transient + + + Private Key + + + SAML Identity Provider Configuration + + + Single Sign On Service URL + + + Single Log Out Service URL + + + Public Key + + + Sign Assertions + + + Signing Algorithm + + + Signing Behavior + + + Binding Type + + + Artifact Resolution Service URL + + + X509 Public Certificate + + + Outbound Signing Algorithm + + + Allow Unsolicited Authentication Response + + + Disable Outbound Logout Requests + + + Want Authentication Requests Signed + + + Metadata Address + + + Get Claims From User Info Endpoint + + + The Authority field is required on a Open ID Connect configuration. + + + The Client ID field is required on a Open ID Connect configuration. + + + The Client Secret field is required on a Open ID Connect configuration. + + + The Callback Path field is required on a Open ID Connect configuration. + + + The Service Provider Configuration Entity Id field is required on a SAML configuration. + + + The Identity Provider Configuration Entity Id field is required on a SAML configuration. + + + If SAML Signing Behavior is set to never, public and private service provider keys are required. + + + If SAML Binding Type is set to artifact, identity provider resolution service URL is required. + + + If Identity Provider Entity ID is not a URL, single sign on service URL is required. + + + The configured authentication scheme is not valid: "{0}" + + + No scheme or handler for this SSO configuration found. + + + SSO is not yet enabled for this organization. + + + No SSO configuration exists for this organization. + + + SSO is not allowed for this organization. + + + Organization not found from identifier. + + + No organization identifier provided. + + + Invalid authentication options provided to SAML2 scheme. + + + Invalid authentication options provided to OpenID Connect scheme. + + + Post configuration not executed against OpenID Connect scheme. + + + Reading OpenID Connect metadata failed. + + + No OpenID Connect metadata could be found or loaded. + + + Error performing pre validation. + + diff --git a/src/Core/Services/II18nService.cs b/src/Core/Services/II18nService.cs index 0b9102c0e..a66e14883 100644 --- a/src/Core/Services/II18nService.cs +++ b/src/Core/Services/II18nService.cs @@ -5,6 +5,7 @@ namespace Bit.Core.Services public interface II18nService { LocalizedString GetLocalizedHtmlString(string key); + LocalizedString GetLocalizedHtmlString(string key, params object[] args); string Translate(string key, params object[] args); string T(string key, params object[] args); } diff --git a/src/Core/Services/Implementations/I18nService.cs b/src/Core/Services/Implementations/I18nService.cs new file mode 100644 index 000000000..fc383f203 --- /dev/null +++ b/src/Core/Services/Implementations/I18nService.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; +using Bit.Core.Resources; +using Microsoft.Extensions.Localization; + +namespace Bit.Core.Services +{ + public class I18nService : II18nService + { + private readonly IStringLocalizer _localizer; + + public I18nService(IStringLocalizerFactory factory) + { + var assemblyName = new AssemblyName(typeof(SharedResources).GetTypeInfo().Assembly.FullName); + _localizer = factory.Create("SharedResources", assemblyName.Name); + } + + public LocalizedString GetLocalizedHtmlString(string key) + { + return _localizer[key]; + } + + public LocalizedString GetLocalizedHtmlString(string key, params object[] args) + { + return _localizer[key, args]; + } + + public string Translate(string key, params object[] args) + { + return string.Format(GetLocalizedHtmlString(key).ToString(), args); + } + + public string T(string key, params object[] args) + { + return Translate(key, args); + } + } +} diff --git a/src/Core/Services/Implementations/I18nViewLocalizer.cs b/src/Core/Services/Implementations/I18nViewLocalizer.cs new file mode 100644 index 000000000..b09435114 --- /dev/null +++ b/src/Core/Services/Implementations/I18nViewLocalizer.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using Bit.Core.Resources; +using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Localization; + +namespace Bit.Core.Services +{ + public class I18nViewLocalizer : IViewLocalizer + { + private readonly IStringLocalizer _stringLocalizer; + private readonly IHtmlLocalizer _htmlLocalizer; + + public I18nViewLocalizer(IStringLocalizerFactory stringFactory, + IHtmlLocalizerFactory htmlFactory) + { + var assemblyName = new AssemblyName(typeof(SharedResources).GetTypeInfo().Assembly.FullName); + _stringLocalizer = stringFactory.Create("SharedResources", assemblyName.Name); + _htmlLocalizer = htmlFactory.Create("SharedResources", assemblyName.Name); + } + + public LocalizedHtmlString this[string name] => _htmlLocalizer[name]; + public LocalizedHtmlString this[string name, params object[] args] => _htmlLocalizer[name, args]; + + public IEnumerable GetAllStrings(bool includeParentCultures) => + _stringLocalizer.GetAllStrings(includeParentCultures); + + public LocalizedString GetString(string name) => _stringLocalizer[name]; + public LocalizedString GetString(string name, params object[] arguments) => + _stringLocalizer[name, arguments]; + + [Obsolete("This method is obsolete. Use `CurrentCulture` and `CurrentUICulture` instead.")] + public IHtmlLocalizer WithCulture(CultureInfo culture) => _htmlLocalizer.WithCulture(culture); + } +} diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index 33e275979..cdb55fca3 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -32,6 +32,9 @@ using AutoMapper; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Azure.Storage; +using System.Reflection; +using Bit.Core.Resources; +using Microsoft.AspNetCore.Mvc.Localization; namespace Bit.Core.Utilities { @@ -453,5 +456,32 @@ namespace Bit.Core.Utilities } app.UseForwardedHeaders(options); } + + public static void AddCoreLocalizationServices(this IServiceCollection services) + { + services.AddTransient(); + services.AddLocalization(options => options.ResourcesPath = "Resources"); + } + + public static IApplicationBuilder UseCoreLocalization(this IApplicationBuilder app) + { + var supportedCultures = new[] { "en" }; + return app.UseRequestLocalization(options => options + .SetDefaultCulture(supportedCultures[0]) + .AddSupportedCultures(supportedCultures) + .AddSupportedUICultures(supportedCultures)); + } + + public static IMvcBuilder AddViewAndDataAnnotationLocalization(this IMvcBuilder mvc) + { + mvc.Services.AddTransient(); + return mvc.AddViewLocalization(options => options.ResourcesPath = "Resources") + .AddDataAnnotationsLocalization(options => + options.DataAnnotationLocalizerProvider = (type, factory) => + { + var assemblyName = new AssemblyName(typeof(SharedResources).GetTypeInfo().Assembly.FullName); + return factory.Create("SharedResources", assemblyName.Name); + }); + } } } diff --git a/src/Identity/Controllers/AccountController.cs b/src/Identity/Controllers/AccountController.cs index 741dc3bb2..ba1d3afa5 100644 --- a/src/Identity/Controllers/AccountController.cs +++ b/src/Identity/Controllers/AccountController.cs @@ -1,4 +1,5 @@ -using Bit.Core.Models.Table; +using Bit.Core.Models.Api; +using Bit.Core.Models.Table; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Identity.Models; @@ -7,13 +8,15 @@ using IdentityServer4; using IdentityServer4.Services; using IdentityServer4.Stores; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Security.Claims; using System.Threading.Tasks; @@ -26,21 +29,63 @@ namespace Bit.Identity.Controllers private readonly ILogger _logger; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IUserRepository _userRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IHttpClientFactory _clientFactory; public AccountController( IClientStore clientStore, IIdentityServerInteractionService interaction, ILogger logger, - IOrganizationUserRepository organizationUserRepository, ISsoConfigRepository ssoConfigRepository, IUserRepository userRepository, - IUserService userService) + IOrganizationRepository organizationRepository, + IHttpClientFactory clientFactory) { _clientStore = clientStore; _interaction = interaction; _logger = logger; _ssoConfigRepository = ssoConfigRepository; _userRepository = userRepository; + _organizationRepository = organizationRepository; + _clientFactory = clientFactory; + } + + [HttpGet] + public async Task PreValidate(string domainHint) + { + if (string.IsNullOrWhiteSpace(domainHint)) + { + Response.StatusCode = 400; + return Json(new ErrorResponseModel("No domain hint was provided")); + } + try + { + // Calls Sso Pre-Validate, assumes baseUri set + var requestCultureFeature = Request.HttpContext.Features.Get(); + var culture = requestCultureFeature.RequestCulture.Culture.Name; + var requestPath = $"/Account/PreValidate?domainHint={domainHint}&culture={culture}"; + var httpClient = _clientFactory.CreateClient("InternalSso"); + using var responseMessage = await httpClient.GetAsync(requestPath); + if (responseMessage.IsSuccessStatusCode) + { + // All is good! + return new EmptyResult(); + } + Response.StatusCode = (int)responseMessage.StatusCode; + var responseJson = await responseMessage.Content.ReadAsStringAsync(); + return Content(responseJson, "application/json"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error pre-validating against SSO service"); + Response.StatusCode = 500; + return Json(new ErrorResponseModel("Error pre-validating SSO authentication") + { + ExceptionMessage = ex.Message, + ExceptionStackTrace = ex.StackTrace, + InnerExceptionMessage = ex.InnerException?.Message, + }); + } } [HttpGet] diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index b0672820a..8d303ff31 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -100,7 +100,10 @@ namespace Bit.Identity { // Pass domain_hint onto the sso idp context.ProtocolMessage.DomainHint = context.Properties.Items["domain_hint"]; - context.ProtocolMessage.SessionState = context.Properties.Items["user_identifier"]; + if (context.Properties.Items.ContainsKey("user_identifier")) + { + context.ProtocolMessage.SessionState = context.Properties.Items["user_identifier"]; + } return Task.FromResult(0); } }; @@ -115,12 +118,19 @@ namespace Bit.Identity // Services services.AddBaseServices(); services.AddDefaultServices(globalSettings); + services.AddCoreLocalizationServices(); if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) && CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName)) { services.AddHostedService(); } + + // HttpClients + services.AddHttpClient("InternalSso", client => + { + client.BaseAddress = new Uri(globalSettings.BaseServiceUri.InternalSso); + }); } public void Configure( @@ -153,6 +163,9 @@ namespace Bit.Identity app.UseCookiePolicy(); } + // Add localization + app.UseCoreLocalization(); + // Add static files to the request pipeline. app.UseStaticFiles(); diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadByIdentifier.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadByIdentifier.sql index 305396483..343592e8e 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_ReadByIdentifier.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadByIdentifier.sql @@ -1,5 +1,5 @@ CREATE PROCEDURE [dbo].[Organization_ReadByIdentifier] - @Identifier UNIQUEIDENTIFIER + @Identifier NVARCHAR(50) AS BEGIN SET NOCOUNT ON @@ -10,4 +10,4 @@ BEGIN [dbo].[OrganizationView] WHERE [Identifier] = @Identifier -END \ No newline at end of file +END diff --git a/util/Migrator/DbScripts/2020-08-12_00_OrgIdentifierProc.sql b/util/Migrator/DbScripts/2020-08-12_00_OrgIdentifierProc.sql index 646a89aeb..1eec9ebf7 100644 --- a/util/Migrator/DbScripts/2020-08-12_00_OrgIdentifierProc.sql +++ b/util/Migrator/DbScripts/2020-08-12_00_OrgIdentifierProc.sql @@ -5,7 +5,7 @@ END GO CREATE PROCEDURE [dbo].[Organization_ReadByIdentifier] - @Identifier UNIQUEIDENTIFIER + @Identifier NVARCHAR(50) AS BEGIN SET NOCOUNT ON diff --git a/util/Migrator/DbScripts/2020-08-28_00_OrgByIdentifierFix.sql b/util/Migrator/DbScripts/2020-08-28_00_OrgByIdentifierFix.sql new file mode 100644 index 000000000..1eec9ebf7 --- /dev/null +++ b/util/Migrator/DbScripts/2020-08-28_00_OrgByIdentifierFix.sql @@ -0,0 +1,20 @@ +IF OBJECT_ID('[dbo].[Organization_ReadByIdentifier]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_ReadByIdentifier] +END +GO + +CREATE PROCEDURE [dbo].[Organization_ReadByIdentifier] + @Identifier NVARCHAR(50) +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationView] + WHERE + [Identifier] = @Identifier +END +GO