1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 08:02:49 -05:00

Fix safari sso header size (#1065)

* Safari SSO header size fix - in progress

* Cleanup of memoryCacheTicketStore

* Redis cache ticket store + registration

* Revert some unecessary changes

* temp - distributed cookie: idsrv.external

* Ticket data cached storage added

* OIDC working w/ substantially reduced cookie size

* Added distributed cache cookie manager

* Removed hybrid OIDC flow

* Enable self-hosted folks to use Redis  for SSO

* Also allow self-hosted to use Redis cont...
This commit is contained in:
Chad Scharf
2021-01-11 11:03:46 -05:00
committed by GitHub
parent 5aba9f7549
commit 99b95b5330
17 changed files with 398 additions and 36 deletions

View File

@ -204,7 +204,7 @@ namespace Bit.Sso.Controllers
{
// Read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme);
Core.AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception(_i18nService.T("ExternalAuthenticationError"));
@ -249,7 +249,7 @@ namespace Bit.Sso.Controllers
}
// Delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
await HttpContext.SignOutAsync(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
// Retrieve return URL
var returnUrl = result.Properties.Items["return_url"] ?? "~/";

View File

@ -7,6 +7,7 @@
<UserSecretsId>bitwarden-Sso</UserSecretsId>
</PropertyGroup>
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Sso' " />
<ItemGroup>
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.7.0" />
</ItemGroup>

View File

@ -58,7 +58,9 @@ namespace Bit.Sso
}
// Authentication
services.AddAuthentication();
services.AddDistributedIdentityServices(globalSettings);
services.AddAuthentication()
.AddCookie(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
services.AddSsoServices(globalSettings);
// IdentityServer

View File

@ -14,8 +14,10 @@ using Bit.Sso.Models;
using Bit.Sso.Utilities;
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Infrastructure;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
@ -40,6 +42,7 @@ namespace Bit.Core.Business.Sso
private readonly Dictionary<string, DynamicAuthenticationScheme> _cachedSchemes;
private readonly Dictionary<string, DynamicAuthenticationScheme> _cachedHandlerSchemes;
private readonly SemaphoreSlim _semaphore;
private readonly IHttpContextAccessor _httpContextAccessor;
private DateTime? _lastSchemeLoad;
private IEnumerable<DynamicAuthenticationScheme> _schemesCopy = Array.Empty<DynamicAuthenticationScheme>();
@ -54,7 +57,8 @@ namespace Bit.Core.Business.Sso
ISsoConfigRepository ssoConfigRepository,
ILogger<DynamicAuthenticationSchemeProvider> logger,
GlobalSettings globalSettings,
SamlEnvironment samlEnvironment)
SamlEnvironment samlEnvironment,
IHttpContextAccessor httpContextAccessor)
: base(options)
{
_oidcPostConfigureOptions = oidcPostConfigureOptions;
@ -81,6 +85,7 @@ namespace Bit.Core.Business.Sso
_cachedSchemes = new Dictionary<string, DynamicAuthenticationScheme>();
_cachedHandlerSchemes = new Dictionary<string, DynamicAuthenticationScheme>();
_semaphore = new SemaphoreSlim(1);
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
private bool CacheIsValid
@ -304,7 +309,7 @@ namespace Bit.Core.Business.Sso
ClientSecret = config.ClientSecret,
ResponseType = "code",
ResponseMode = "form_post",
SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,
SignOutScheme = IdentityServerConstants.SignoutScheme,
SaveTokens = false, // reduce overall request size
TokenValidationParameters = new TokenValidationParameters
@ -332,6 +337,8 @@ namespace Bit.Core.Business.Sso
oidcOptions.Scope.Add(OpenIdConnectScopes.Profile);
}
oidcOptions.StateDataFormat = new DistributedCacheStateDataFormatter(_httpContextAccessor, name);
return new DynamicAuthenticationScheme(name, name, typeof(OpenIdConnectHandler),
oidcOptions, SsoType.OpenIdConnect);
}
@ -407,8 +414,9 @@ namespace Bit.Core.Business.Sso
var options = new Saml2Options
{
SPOptions = spOptions,
SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,
SignOutScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme,
CookieManager = new IdentityServer.DistributedCacheCookieManager(),
};
options.IdentityProviders.Add(idp);

View File

@ -33,8 +33,6 @@ namespace Bit.Api
public void ConfigureServices(IServiceCollection services)
{
var provider = services.BuildServiceProvider();
// Options
services.AddOptions();

View File

@ -9,4 +9,9 @@
{
public const string LinkSso = "LinkSso";
}
public static class AuthenticationSchemes
{
public const string BitwardenExternalCookieAuthenticationScheme = "bw.external";
}
}

View File

@ -58,6 +58,7 @@
<PackageReference Include="Otp.NET" Version="1.2.2" />
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
<PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Redis" Version="2.2.0" />
</ItemGroup>
<ItemGroup>

View File

@ -141,6 +141,7 @@ namespace Bit.Core
{
public string CertificateThumbprint { get; set; }
public string CertificatePassword { get; set; }
public string RedisConnectionString { get; set; }
}
public class DataProtectionSettings

View File

@ -0,0 +1,53 @@
using System;
using IdentityServer4.Configuration;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Redis;
using Microsoft.Extensions.Options;
namespace Bit.Core.IdentityServer
{
public class ConfigureOpenIdConnectDistributedOptions : IPostConfigureOptions<CookieAuthenticationOptions>
{
private readonly IdentityServerOptions _idsrv;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly GlobalSettings _globalSettings;
public ConfigureOpenIdConnectDistributedOptions(IHttpContextAccessor httpContextAccessor, GlobalSettings globalSettings,
IdentityServerOptions idsrv)
{
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
_globalSettings = globalSettings;
_idsrv = idsrv;
}
public void PostConfigure(string name, CookieAuthenticationOptions options)
{
options.CookieManager = new DistributedCacheCookieManager();
if (name != AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)
{
// Ignore
return;
}
options.Cookie.Name = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme;
options.Cookie.IsEssential = true;
options.Cookie.SameSite = _idsrv.Authentication.CookieSameSiteMode;
options.TicketDataFormat = new DistributedCacheTicketDataFormatter(_httpContextAccessor, name);
if (string.IsNullOrWhiteSpace(_globalSettings.IdentityServer?.RedisConnectionString))
{
options.SessionStore = new MemoryCacheTicketStore();
}
else
{
var redisOptions = new RedisCacheOptions
{
Configuration = _globalSettings.IdentityServer.RedisConnectionString,
};
options.SessionStore = new RedisCacheTicketStore(redisOptions);
}
}
}
}

View File

@ -0,0 +1,70 @@
using System;
using System.Text;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.IdentityServer
{
public class DistributedCacheCookieManager : ICookieManager
{
private readonly ChunkingCookieManager _cookieManager;
public DistributedCacheCookieManager()
{
_cookieManager = new ChunkingCookieManager();
}
private string CacheKeyPrefix => "cookie-data";
public void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)
{
var id = Guid.NewGuid().ToString();
var cacheKey = GetKey(key, id);
var expiresUtc = options.Expires ?? DateTimeOffset.UtcNow.AddMinutes(15);
var cacheOptions = new DistributedCacheEntryOptions()
.SetAbsoluteExpiration(expiresUtc);
var data = Encoding.UTF8.GetBytes(value);
var cache = GetCache(context);
cache.Set(cacheKey, data, cacheOptions);
// Write the cookie with the identifier as the body
_cookieManager.AppendResponseCookie(context, key, id, options);
}
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
{
_cookieManager.DeleteCookie(context, key, options);
var id = GetId(context, key);
if (!string.IsNullOrWhiteSpace(id))
{
var cacheKey = GetKey(key, id);
GetCache(context).Remove(cacheKey);
}
}
public string GetRequestCookie(HttpContext context, string key)
{
var id = GetId(context, key);
if (string.IsNullOrWhiteSpace(id))
{
return null;
}
var cacheKey = GetKey(key, id);
return GetCache(context).GetString(cacheKey);
}
private IDistributedCache GetCache(HttpContext context) =>
context.RequestServices.GetRequiredService<IDistributedCache>();
private string GetKey(string key, string id) => $"{CacheKeyPrefix}-{key}-{id}";
private string GetId(HttpContext context, string key) =>
context.Request.Cookies.ContainsKey(key) ?
context.Request.Cookies[key] : null;
}
}

View File

@ -0,0 +1,66 @@
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.IdentityServer
{
public class DistributedCacheTicketDataFormatter : ISecureDataFormat<AuthenticationTicket>
{
private readonly IHttpContextAccessor _httpContext;
private readonly string _name;
public DistributedCacheTicketDataFormatter(IHttpContextAccessor httpContext, string name)
{
_httpContext = httpContext;
_name = name;
}
private string CacheKeyPrefix => "ticket-data";
private IDistributedCache Cache => _httpContext.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();
private IDataProtector Protector => _httpContext.HttpContext.RequestServices.GetRequiredService<IDataProtectionProvider>()
.CreateProtector(CacheKeyPrefix, _name);
public string Protect(AuthenticationTicket data) => Protect(data, null);
public string Protect(AuthenticationTicket data, string purpose)
{
var key = Guid.NewGuid().ToString();
var cacheKey = $"{CacheKeyPrefix}-{_name}-{purpose}-{key}";
var expiresUtc = data.Properties.ExpiresUtc ??
DateTimeOffset.UtcNow.AddMinutes(15);
var options = new DistributedCacheEntryOptions();
options.SetAbsoluteExpiration(expiresUtc);
var ticket = TicketSerializer.Default.Serialize(data);
Cache.Set(cacheKey, ticket, options);
return Protector.Protect(key);
}
public AuthenticationTicket Unprotect(string protectedText) => Unprotect(protectedText, null);
public AuthenticationTicket Unprotect(string protectedText, string purpose)
{
if (string.IsNullOrWhiteSpace(protectedText))
{
return null;
}
// Decrypt the key and retrieve the data from the cache.
var key = Protector.Unprotect(protectedText);
var cacheKey = $"{CacheKeyPrefix}-{_name}-{purpose}-{key}";
var ticket = Cache.Get(cacheKey);
if (ticket == null)
{
return null;
}
var data = TicketSerializer.Default.Deserialize(ticket);
return data;
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Caching.Memory;
namespace Bit.Core.IdentityServer
{
public class MemoryCacheTicketStore : ITicketStore
{
private const string _keyPrefix = "auth-";
private readonly IMemoryCache _cache;
public MemoryCacheTicketStore()
{
_cache = new MemoryCache(new MemoryCacheOptions());
}
public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
var key = $"{_keyPrefix}{Guid.NewGuid()}";
await RenewAsync(key, ticket);
return key;
}
public Task RenewAsync(string key, AuthenticationTicket ticket)
{
var options = new MemoryCacheEntryOptions();
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc.HasValue)
{
options.SetAbsoluteExpiration(expiresUtc.Value);
}
else
{
options.SetSlidingExpiration(TimeSpan.FromMinutes(15));
}
_cache.Set(key, ticket, options);
return Task.FromResult(0);
}
public Task<AuthenticationTicket> RetrieveAsync(string key)
{
_cache.TryGetValue(key, out AuthenticationTicket ticket);
return Task.FromResult(ticket);
}
public Task RemoveAsync(string key)
{
_cache.Remove(key);
return Task.FromResult(0);
}
}
}

View File

@ -14,8 +14,8 @@ namespace Bit.Core.IdentityServer
ClientSecrets = new List<Secret> { new Secret(globalSettings.OidcIdentityClientKey.Sha256()) };
AllowedScopes = new string[]
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
};
AllowedGrantTypes = GrantTypes.Code;
Enabled = true;

View File

@ -0,0 +1,68 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.Redis;
namespace Bit.Core.IdentityServer
{
public class RedisCacheTicketStore : ITicketStore
{
private const string _keyPrefix = "auth-";
private readonly IDistributedCache _cache;
public RedisCacheTicketStore(RedisCacheOptions options)
{
_cache = new RedisCache(options);
}
public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
var key = $"{_keyPrefix}{Guid.NewGuid()}";
await RenewAsync(key, ticket);
return key;
}
public Task RenewAsync(string key, AuthenticationTicket ticket)
{
var options = new DistributedCacheEntryOptions();
var expiresUtc = ticket.Properties.ExpiresUtc ??
DateTimeOffset.UtcNow.AddMinutes(15);
options.SetAbsoluteExpiration(expiresUtc);
var val = SerializeToBytes(ticket);
_cache.Set(key, val, options);
return Task.FromResult(0);
}
public Task<AuthenticationTicket> RetrieveAsync(string key)
{
AuthenticationTicket ticket;
var bytes = _cache.Get(key);
ticket = DeserializeFromBytes(bytes);
return Task.FromResult(ticket);
}
public Task RemoveAsync(string key)
{
_cache.Remove(key);
return Task.FromResult(0);
}
private static byte[] SerializeToBytes(AuthenticationTicket source)
{
return TicketSerializer.Default.Serialize(source);
}
private static AuthenticationTicket DeserializeFromBytes(byte[] source)
{
return source == null ? null : TicketSerializer.Default.Deserialize(source);
}
}
}

View File

@ -1,40 +1,43 @@
using Bit.Core.Enums;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using AutoMapper;
using Bit.Core.Enums;
using Bit.Core.Identity;
using Bit.Core.IdentityServer;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Bit.Core.Resources;
using Bit.Core.Services;
using Bit.Core.Utilities;
using IdentityModel;
using IdentityServer4.AccessTokenValidation;
using IdentityServer4.Configuration;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Azure.Storage;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using System;
using System.IO;
using SqlServerRepos = Bit.Core.Repositories.SqlServer;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Serilog.Context;
using EntityFrameworkRepos = Bit.Core.Repositories.EntityFramework;
using NoopRepos = Bit.Core.Repositories.Noop;
using System.Threading.Tasks;
using SqlServerRepos = Bit.Core.Repositories.SqlServer;
using TableStorageRepos = Bit.Core.Repositories.TableStorage;
using Microsoft.Extensions.DependencyInjection.Extensions;
using IdentityServer4.AccessTokenValidation;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.HttpOverrides;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Bit.Core.Utilities;
using Serilog.Context;
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
{
@ -502,5 +505,31 @@ namespace Bit.Core.Utilities
return factory.Create("SharedResources", assemblyName.Name);
});
}
public static IServiceCollection AddDistributedIdentityServices(this IServiceCollection services, GlobalSettings globalSettings)
{
if (string.IsNullOrWhiteSpace(globalSettings.IdentityServer?.RedisConnectionString))
{
services.AddDistributedMemoryCache();
}
else
{
services.AddDistributedRedisCache(options =>
options.Configuration = globalSettings.IdentityServer.RedisConnectionString);
}
services.AddOidcStateDataFormatterCache();
services.AddSession();
services.ConfigureApplicationCookie(configure => configure.CookieManager = new DistributedCacheCookieManager());
services.ConfigureExternalCookie(configure => configure.CookieManager = new DistributedCacheCookieManager());
services.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>>(
svcs => new ConfigureOpenIdConnectDistributedOptions(
svcs.GetRequiredService<IHttpContextAccessor>(),
globalSettings,
svcs.GetRequiredService<IdentityServerOptions>())
);
return services;
}
}
}

View File

@ -153,7 +153,7 @@ namespace Bit.Identity.Controllers
{
// Read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme);
Core.AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
@ -190,7 +190,7 @@ namespace Bit.Identity.Controllers
}, localSignInProps);
// Delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
await HttpContext.SignOutAsync(Core.AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
// Retrieve return URL
var returnUrl = result.Properties.Items["return_url"] ?? "~/";

View File

@ -81,7 +81,9 @@ namespace Bit.Identity
// Authentication
services
.AddDistributedIdentityServices(globalSettings)
.AddAuthentication()
.AddCookie(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme)
.AddOpenIdConnect("sso", "Single Sign On", options =>
{
options.Authority = globalSettings.BaseServiceUri.InternalSso;
@ -91,8 +93,10 @@ namespace Bit.Identity
options.ClientSecret = globalSettings.OidcIdentityClientKey;
options.ResponseMode = "form_post";
options.SignInScheme = IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme;
options.ResponseType = "code";
options.SaveTokens = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents
{