From 3daf0bcd18b8f950c9854005a1bcfcdfdaec976f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 5 May 2017 20:57:33 -0400 Subject: [PATCH] centralize a lot of service registration --- src/Api/Api.csproj | 12 +- src/Api/Startup.cs | 109 +----------- src/Billing/Billing.csproj | 2 - src/Billing/Startup.cs | 23 +-- src/Billing/settings.json | 6 - src/Core/Core.csproj | 7 +- src/Core/ServiceCollectionExtensions.cs | 47 ----- src/Core/Utilities/LoggerFactoryExtensions.cs | 39 +++++ .../Utilities/ServiceCollectionExtensions.cs | 165 ++++++++++++++++++ src/Identity/Identity.csproj | 3 + src/Identity/Startup.cs | 66 ++++++- src/Identity/settings.Preview.json | 5 + src/Identity/settings.Production.json | 5 + src/Identity/settings.Staging.json | 5 + src/Identity/settings.json | 35 ++++ src/Mail/Mail.csproj | 1 - src/Mail/Startup.cs | 1 - 17 files changed, 348 insertions(+), 183 deletions(-) delete mode 100644 src/Core/ServiceCollectionExtensions.cs create mode 100644 src/Core/Utilities/LoggerFactoryExtensions.cs create mode 100644 src/Core/Utilities/ServiceCollectionExtensions.cs create mode 100644 src/Identity/settings.Preview.json create mode 100644 src/Identity/settings.Production.json create mode 100644 src/Identity/settings.Staging.json create mode 100644 src/Identity/settings.json diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 9e8595eb04..e2f14e8508 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -22,23 +22,13 @@ - - + - - - - - - - - - diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 5f12d44151..609ffbcca5 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -3,7 +3,6 @@ using System.Security.Claims; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -11,26 +10,17 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Bit.Api.Utilities; using Bit.Core; -using Bit.Core.Models.Table; using Bit.Core.Identity; -using System.Text; using System.Linq; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; using Newtonsoft.Json.Serialization; using AspNetCoreRateLimit; using Bit.Api.Middleware; -using IdentityServer4.Validation; -using IdentityServer4.Services; -using IdentityServer4.Stores; -using Bit.Core.Utilities; -using Serilog; using Serilog.Events; using Bit.Core.IdentityServer; -using Bit.Core.Enums; -using Microsoft.AspNetCore.DataProtection; -using Microsoft.WindowsAzure.Storage; using Stripe; +using Bit.Core.Utilities; namespace Bit.Api { @@ -65,21 +55,12 @@ namespace Bit.Api services.AddOptions(); // Settings - var globalSettings = new GlobalSettings(); - ConfigurationBinder.Bind(Configuration.GetSection("GlobalSettings"), globalSettings); - services.AddSingleton(s => globalSettings); + var globalSettings = services.AddGlobalSettingsServices(Configuration); services.Configure(Configuration.GetSection("IpRateLimitOptions")); services.Configure(Configuration.GetSection("IpRateLimitPolicies")); // Data Protection - if(Environment.IsProduction()) - { - var dataProtectionCert = CoreHelpers.GetCertificate(globalSettings.DataProtection.CertificateThumbprint); - var storageAccount = CloudStorageAccount.Parse(globalSettings.Storage.ConnectionString); - services.AddDataProtection() - .PersistKeysToAzureBlobStorage(storageAccount, "aspnet-dataprotection/keys.xml") - .ProtectKeysWithCertificate(dataProtectionCert); - } + services.AddCustomDataProtectionServices(Environment, globalSettings); // Stripe Billing StripeConfiguration.SetApiKey(globalSettings.StripeApiKey); @@ -98,69 +79,10 @@ namespace Bit.Api services.AddSingleton(); // IdentityServer - 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; - }) - .AddInMemoryApiResources(ApiResources.GetApiResources()) - .AddInMemoryClients(Clients.GetClients()); - services.AddTransient(); - - if(Environment.IsProduction()) - { - var identityServerCert = CoreHelpers.GetCertificate(globalSettings.IdentityServer.CertificateThumbprint); - identityServerBuilder.AddSigningCredential(identityServerCert); - } - else - { - identityServerBuilder.AddTemporarySigningCredential(); - } - - services.AddScoped(); - services.AddScoped(); - services.AddSingleton(); + services.AddCustomIdentityServerServices(Environment, globalSettings); // Identity - services.AddTransient(); - services.AddJwtBearerIdentity(options => - { - options.User = new UserOptions - { - RequireUniqueEmail = true, - AllowedUserNameCharacters = null // all - }; - options.Password = new PasswordOptions - { - RequireDigit = false, - RequireLowercase = false, - RequiredLength = 8, - RequireNonAlphanumeric = false, - RequireUppercase = false - }; - options.ClaimsIdentity = new ClaimsIdentityOptions - { - SecurityStampClaimType = "securitystamp", - UserNameClaimType = ClaimTypes.Email - }; - options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider; - }, jwtBearerOptions => - { - jwtBearerOptions.Audience = "bitwarden"; - jwtBearerOptions.Issuer = "bitwarden"; - jwtBearerOptions.TokenLifetime = TimeSpan.FromDays(10 * 365); - jwtBearerOptions.TwoFactorTokenLifetime = TimeSpan.FromMinutes(10); - var keyBytes = Encoding.ASCII.GetBytes(globalSettings.JwtSigningKey); - jwtBearerOptions.SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(keyBytes), SecurityAlgorithms.HmacSha256); - }) - .AddUserStore() - .AddRoleStore() - .AddTokenProvider(TwoFactorProviderType.Authenticator.ToString()) - .AddTokenProvider>(TokenOptions.DefaultEmailProvider); + services.AddCustomIdentityServices(globalSettings); var jwtIdentityOptions = provider.GetRequiredService>().Value; services.AddAuthorization(config => @@ -215,9 +137,8 @@ namespace Bit.Api IApplicationLifetime appLifetime, GlobalSettings globalSettings) { - if(env.IsProduction()) - { - Func serilogFilter = (e) => + loggerFactory + .AddSerilog(env, appLifetime, globalSettings, (e) => { var context = e.Properties["SourceContext"].ToString(); if(e.Exception != null && (e.Exception.GetType() == typeof(SecurityTokenValidationException) || @@ -237,20 +158,8 @@ namespace Bit.Api } return e.Level >= LogEventLevel.Error; - }; - - var serilog = new LoggerConfiguration() - .Enrich.FromLogContext() - .Filter.ByIncludingOnly(serilogFilter) - .WriteTo.AzureDocumentDB(new Uri(globalSettings.DocumentDb.Uri), globalSettings.DocumentDb.Key, - timeToLive: TimeSpan.FromDays(7)) - .CreateLogger(); - - loggerFactory.AddSerilog(serilog); - appLifetime.ApplicationStopped.Register(Log.CloseAndFlush); - } - - loggerFactory.AddDebug(); + }) + .AddDebug(); // Rate limiting app.UseMiddleware(); diff --git a/src/Billing/Billing.csproj b/src/Billing/Billing.csproj index 55a71a9037..4a3d37169f 100644 --- a/src/Billing/Billing.csproj +++ b/src/Billing/Billing.csproj @@ -24,8 +24,6 @@ - - diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index 5007536ff6..a5bac1925c 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -9,6 +6,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Bit.Core; using Stripe; +using Bit.Core.Utilities; +using Serilog.Events; namespace Bit.Billing { @@ -39,10 +38,7 @@ namespace Bit.Billing services.AddOptions(); // Settings - var globalSettings = new GlobalSettings(); - ConfigurationBinder.Bind(Configuration.GetSection("GlobalSettings"), globalSettings); - services.AddSingleton(s => globalSettings); - services.Configure(Configuration.GetSection("BillingSettings")); + var globalSettings = services.AddGlobalSettingsServices(Configuration); // Stripe Billing StripeConfiguration.SetApiKey(globalSettings.StripeApiKey); @@ -61,10 +57,17 @@ namespace Bit.Billing services.AddMvc(); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public void Configure( + IApplicationBuilder app, + IHostingEnvironment env, + IApplicationLifetime appLifetime, + GlobalSettings globalSettings, + ILoggerFactory loggerFactory) { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); + loggerFactory + .AddSerilog(env, appLifetime, globalSettings, (e) => e.Level >= LogEventLevel.Error) + .AddConsole() + .AddDebug(); app.UseMvc(); } diff --git a/src/Billing/settings.json b/src/Billing/settings.json index 2fa3e9db00..3414ba4825 100644 --- a/src/Billing/settings.json +++ b/src/Billing/settings.json @@ -1,10 +1,4 @@ { - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Warning" - } - }, "globalSettings": { "siteName": "bitwarden", "baseVaultUri": "http://localhost:4001/#", diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 22ed7c4db7..42f42e08bb 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -15,13 +15,16 @@ + - - + + + + diff --git a/src/Core/ServiceCollectionExtensions.cs b/src/Core/ServiceCollectionExtensions.cs deleted file mode 100644 index 0af68ce060..0000000000 --- a/src/Core/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Bit.Core.Repositories; -using Bit.Core.Services; -using Microsoft.Extensions.DependencyInjection; -using SqlServerRepos = Bit.Core.Repositories.SqlServer; - -namespace Bit.Core -{ - public static class ServiceCollectionExtensions - { - public static void AddSqlServerRepositories(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - } - - public static void AddBaseServices(this IServiceCollection services) - { - services.AddSingleton(); - services.AddScoped(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - } - - public static void AddDefaultServices(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - } - - public static void AddNoopServices(this IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - } - } -} diff --git a/src/Core/Utilities/LoggerFactoryExtensions.cs b/src/Core/Utilities/LoggerFactoryExtensions.cs new file mode 100644 index 0000000000..596ce6b428 --- /dev/null +++ b/src/Core/Utilities/LoggerFactoryExtensions.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Events; +using System; + +namespace Bit.Core.Utilities +{ + public static class LoggerFactoryExtensions + { + public static ILoggerFactory AddSerilog( + this ILoggerFactory factory, + IHostingEnvironment env, + IApplicationLifetime appLifetime, + GlobalSettings globalSettings, + Func filter = null) + { + if(env.IsProduction()) + { + if(filter == null) + { + filter = (e) => true; + } + + var serilog = new LoggerConfiguration() + .Enrich.FromLogContext() + .Filter.ByIncludingOnly(filter) + .WriteTo.AzureDocumentDB(new Uri(globalSettings.DocumentDb.Uri), globalSettings.DocumentDb.Key, + timeToLive: TimeSpan.FromDays(7)) + .CreateLogger(); + + factory.AddSerilog(serilog); + appLifetime.ApplicationStopped.Register(Log.CloseAndFlush); + } + + return factory; + } + } +} diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..ee2958feea --- /dev/null +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -0,0 +1,165 @@ +using Bit.Core.Enums; +using Bit.Core.Identity; +using Bit.Core.IdentityServer; +using Bit.Core.Models.Table; +using Bit.Core.Repositories; +using Bit.Core.Services; +using IdentityServer4.Services; +using IdentityServer4.Stores; +using IdentityServer4.Validation; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using Microsoft.WindowsAzure.Storage; +using System; +using System.Security.Claims; +using System.Text; +using SqlServerRepos = Bit.Core.Repositories.SqlServer; + +namespace Bit.Core.Utilities +{ + public static class ServiceCollectionExtensions + { + public static void AddSqlServerRepositories(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + public static void AddBaseServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + public static void AddDefaultServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + public static void AddNoopServices(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + public static IdentityBuilder AddCustomIdentityServices( + this IServiceCollection services, GlobalSettings globalSettings) + { + services.AddTransient(); + + var identityBuilder = services.AddJwtBearerIdentity(options => + { + options.User = new UserOptions + { + RequireUniqueEmail = true, + AllowedUserNameCharacters = null // all + }; + options.Password = new PasswordOptions + { + RequireDigit = false, + RequireLowercase = false, + RequiredLength = 8, + RequireNonAlphanumeric = false, + RequireUppercase = false + }; + options.ClaimsIdentity = new ClaimsIdentityOptions + { + SecurityStampClaimType = "securitystamp", + UserNameClaimType = ClaimTypes.Email + }; + options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider; + }, jwtBearerOptions => + { + jwtBearerOptions.Audience = "bitwarden"; + jwtBearerOptions.Issuer = "bitwarden"; + jwtBearerOptions.TokenLifetime = TimeSpan.FromDays(10 * 365); + jwtBearerOptions.TwoFactorTokenLifetime = TimeSpan.FromMinutes(10); + var keyBytes = Encoding.ASCII.GetBytes(globalSettings.JwtSigningKey); + jwtBearerOptions.SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(keyBytes), SecurityAlgorithms.HmacSha256); + }); + + identityBuilder + .AddUserStore() + .AddRoleStore() + .AddTokenProvider(TwoFactorProviderType.Authenticator.ToString()) + .AddTokenProvider>(TokenOptions.DefaultEmailProvider); + + return identityBuilder; + } + + public static IIdentityServerBuilder AddCustomIdentityServerServices( + this IServiceCollection services, IHostingEnvironment env, GlobalSettings globalSettings) + { + 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; + }) + .AddInMemoryApiResources(ApiResources.GetApiResources()) + .AddInMemoryClients(Clients.GetClients()); + + services.AddTransient(); + + if(env.IsProduction()) + { + var identityServerCert = CoreHelpers.GetCertificate(globalSettings.IdentityServer.CertificateThumbprint); + identityServerBuilder.AddSigningCredential(identityServerCert); + } + else + { + identityServerBuilder.AddTemporarySigningCredential(); + } + + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + + return identityServerBuilder; + } + + public static void AddCustomDataProtectionServices( + this IServiceCollection services, IHostingEnvironment env, GlobalSettings globalSettings) + { + if(env.IsProduction()) + { + var dataProtectionCert = CoreHelpers.GetCertificate(globalSettings.DataProtection.CertificateThumbprint); + var storageAccount = CloudStorageAccount.Parse(globalSettings.Storage.ConnectionString); + services.AddDataProtection() + .PersistKeysToAzureBlobStorage(storageAccount, "aspnet-dataprotection/keys.xml") + .ProtectKeysWithCertificate(dataProtectionCert); + } + } + + public static GlobalSettings AddGlobalSettingsServices(this IServiceCollection services, + IConfigurationRoot root) + { + var globalSettings = new GlobalSettings(); + ConfigurationBinder.Bind(root.GetSection("GlobalSettings"), globalSettings); + services.AddSingleton(s => globalSettings); + return globalSettings; + } + } +} diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj index 47dd9bbf4f..d659ae41b7 100644 --- a/src/Identity/Identity.csproj +++ b/src/Identity/Identity.csproj @@ -4,10 +4,13 @@ net461 Identity Bit.Identity + 527b1fab-a4b5-465b-881a-f44f08c42899 + + diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 21a55bdab3..e6dcd05a00 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -4,24 +4,84 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; +using Bit.Core; +using Bit.Core.Utilities; +using Serilog.Events; namespace Bit.Identity { public class Startup { - public void ConfigureServices(IServiceCollection services) + public Startup(IHostingEnvironment env) { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("settings.json") + .AddJsonFile($"settings.{env.EnvironmentName}.json", optional: true); + + if(env.IsDevelopment()) + { + builder.AddUserSecrets(); + } + + builder.AddEnvironmentVariables(); + + Configuration = builder.Build(); + Environment = env; } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public IConfigurationRoot Configuration { get; private set; } + public IHostingEnvironment Environment { get; set; } + + public void ConfigureServices(IServiceCollection services) { - loggerFactory.AddConsole(); + // Options + services.AddOptions(); + + // Settings + var globalSettings = services.AddGlobalSettingsServices(Configuration); + + // Data Protection + services.AddCustomDataProtectionServices(Environment, globalSettings); + + // Repositories + services.AddSqlServerRepositories(); + + // Context + services.AddScoped(); + + // IdentityServer + services.AddCustomIdentityServerServices(Environment, globalSettings); + + // Identity + services.AddCustomIdentityServices(globalSettings); + + // Services + services.AddBaseServices(); + services.AddDefaultServices(); + } + + public void Configure( + IApplicationBuilder app, + IHostingEnvironment env, + ILoggerFactory loggerFactory, + IApplicationLifetime appLifetime, + GlobalSettings globalSettings) + { + loggerFactory + .AddSerilog(env, appLifetime, globalSettings, (e) => e.Level >= LogEventLevel.Error) + .AddConsole() + .AddDebug(); if(env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } + // Add IdentityServer to the request pipeline. + app.UseIdentityServer(); + app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); diff --git a/src/Identity/settings.Preview.json b/src/Identity/settings.Preview.json new file mode 100644 index 0000000000..3e61d2d4ce --- /dev/null +++ b/src/Identity/settings.Preview.json @@ -0,0 +1,5 @@ +{ + "globalSettings": { + "baseVaultUri": "https://preview-vault.bitwarden.com/#" + } +} diff --git a/src/Identity/settings.Production.json b/src/Identity/settings.Production.json new file mode 100644 index 0000000000..8cb3e0000a --- /dev/null +++ b/src/Identity/settings.Production.json @@ -0,0 +1,5 @@ +{ + "globalSettings": { + "baseVaultUri": "https://vault.bitwarden.com/#" + } +} diff --git a/src/Identity/settings.Staging.json b/src/Identity/settings.Staging.json new file mode 100644 index 0000000000..8cb3e0000a --- /dev/null +++ b/src/Identity/settings.Staging.json @@ -0,0 +1,5 @@ +{ + "globalSettings": { + "baseVaultUri": "https://vault.bitwarden.com/#" + } +} diff --git a/src/Identity/settings.json b/src/Identity/settings.json new file mode 100644 index 0000000000..4470a98cd0 --- /dev/null +++ b/src/Identity/settings.json @@ -0,0 +1,35 @@ +{ + "globalSettings": { + "siteName": "bitwarden", + "baseVaultUri": "http://localhost:4001/#", + "jwtSigningKey": "THIS IS A SECRET. IT KEEPS YOUR TOKEN SAFE. :)", + "stripeApiKey": "SECRET", + "sqlServer": { + "connectionString": "SECRET" + }, + "mail": { + "apiKey": "SECRET", + "replyToEmail": "hello@bitwarden.com" + }, + "push": { + "apnsCertificateThumbprint": "SECRET", + "apnsCertificatePassword": "SECRET", + "gcmSenderId": "SECRET", + "gcmApiKey": "SECRET", + "gcmAppPackageName": "com.x8bit.bitwarden" + }, + "identityServer": { + "certificateThumbprint": "SECRET" + }, + "dataProtection": { + "certificateThumbprint": "SECRET" + }, + "storage": { + "connectionString": "SECRET" + }, + "documentDb": { + "uri": "SECRET", + "key": "SECRET" + } + } +} diff --git a/src/Mail/Mail.csproj b/src/Mail/Mail.csproj index 381e1a0f19..9ea040ab2f 100644 --- a/src/Mail/Mail.csproj +++ b/src/Mail/Mail.csproj @@ -21,7 +21,6 @@ - diff --git a/src/Mail/Startup.cs b/src/Mail/Startup.cs index 0435a6e9f1..9fba56ccbf 100644 --- a/src/Mail/Startup.cs +++ b/src/Mail/Startup.cs @@ -10,7 +10,6 @@ namespace Bit.Mail public void Configure(IApplicationBuilder app) { app.UseFileServer(); - app.UseBrowserLink(); } } }