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