1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

Merge SSO and Portal projects

This commit is contained in:
Kyle Spearrin 2020-09-04 13:56:08 -04:00
parent 61dff9c758
commit 84c85a90e8
173 changed files with 73510 additions and 1 deletions

View File

@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190 VisualStudioVersion = 16.0.29102.190
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src - AGPL", "src - AGPL", "{DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "util", "util", "{DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "util", "util", "{DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}"
EndProject EndProject
@ -53,6 +53,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrator", "util\Migrator\M
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Api.Test", "test\Api.Test\Api.Test.csproj", "{860DE301-0B3E-4717-9C21-A9B4C3C2B121}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Api.Test", "test\Api.Test\Api.Test.csproj", "{860DE301-0B3E-4717-9C21-A9B4C3C2B121}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src - Bitwarden License", "src - Bitwarden License", "{4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Portal", "bitwarden_license\src\Portal\Portal.csproj", "{BA852F18-852F-4154-973B-77D577B8CA04}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sso", "bitwarden_license\src\Sso\Sso.csproj", "{4866AF64-6640-4C65-A662-A31E02FF9064}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -121,6 +127,14 @@ Global
{860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Debug|Any CPU.Build.0 = Debug|Any CPU {860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Debug|Any CPU.Build.0 = Debug|Any CPU
{860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Release|Any CPU.ActiveCfg = Release|Any CPU {860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Release|Any CPU.ActiveCfg = Release|Any CPU
{860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Release|Any CPU.Build.0 = Release|Any CPU {860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Release|Any CPU.Build.0 = Release|Any CPU
{BA852F18-852F-4154-973B-77D577B8CA04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA852F18-852F-4154-973B-77D577B8CA04}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA852F18-852F-4154-973B-77D577B8CA04}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA852F18-852F-4154-973B-77D577B8CA04}.Release|Any CPU.Build.0 = Release|Any CPU
{4866AF64-6640-4C65-A662-A31E02FF9064}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4866AF64-6640-4C65-A662-A31E02FF9064}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4866AF64-6640-4C65-A662-A31E02FF9064}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4866AF64-6640-4C65-A662-A31E02FF9064}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -141,6 +155,8 @@ Global
{79BB453F-D0D8-4DDF-9809-A405C56692BD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D} {79BB453F-D0D8-4DDF-9809-A405C56692BD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
{54DED792-A022-417E-9804-21FCC9C7C610} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} {54DED792-A022-417E-9804-21FCC9C7C610} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{860DE301-0B3E-4717-9C21-A9B4C3C2B121} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {860DE301-0B3E-4717-9C21-A9B4C3C2B121} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{BA852F18-852F-4154-973B-77D577B8CA04} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
{4866AF64-6640-4C65-A662-A31E02FF9064} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}

View File

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Portal.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Bit.Portal.Components
{
public class OrganizationPickerViewComponent : ViewComponent
{
private readonly EnterprisePortalCurrentContext _enterprisePortalCurrentContext;
public OrganizationPickerViewComponent(EnterprisePortalCurrentContext enterprisePortalCurrentContext)
{
_enterprisePortalCurrentContext = enterprisePortalCurrentContext;
}
public Task<IViewComponentResult> InvokeAsync()
{
return Task.FromResult(View(new OrganizationPickerViewModel
{
SelectedOrganization = _enterprisePortalCurrentContext?.SelectedOrganizationId?.ToString(),
Organizations = _enterprisePortalCurrentContext?.OrganizationsDetails?.Where(x => x.UseBusinessPortal)
.Select(o => new SelectListItem
{
Value = o.OrganizationId.ToString(),
Text = o.Name
}).ToList() ?? new List<SelectListItem>()
}) as IViewComponentResult);
}
}
}

View File

@ -0,0 +1,63 @@
using System.Threading.Tasks;
using Bit.Portal.Utilities;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Portal.Controllers
{
public class AuthController : Controller
{
private readonly EnterprisePortalTokenSignInManager _signInManager;
public AuthController(
EnterprisePortalTokenSignInManager signInManager)
{
_signInManager = signInManager;
}
[HttpGet("~/login")]
public async Task<IActionResult> Index(string userId, string token, string organizationId, string returnUrl)
{
var result = await _signInManager.TokenSignInAsync(userId, token, false);
if (!result.Succeeded)
{
return RedirectToAction("Index", "Home", new
{
error = 2
});
}
if (!string.IsNullOrWhiteSpace(organizationId))
{
Response.Cookies.Append("SelectedOrganization", organizationId, new CookieOptions { HttpOnly = true });
}
if (!string.IsNullOrWhiteSpace(returnUrl) && Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}
[HttpPost("~/logout")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
return RedirectToAction("LoggedOut");
}
[HttpGet("~/logged-out")]
public IActionResult LoggedOut()
{
return View();
}
[HttpGet("~/access-denied")]
public IActionResult AccessDenied()
{
return View();
}
}
}

View File

@ -0,0 +1,70 @@
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Bit.Portal.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Bit.Core.Models.Table;
namespace Bit.Portal.Controllers
{
public class HomeController : Controller
{
private readonly SignInManager<User> _signInManager;
private readonly ILogger<HomeController> _logger;
private readonly EnterprisePortalCurrentContext _enterprisePortalCurrentContext;
public HomeController(
SignInManager<User> signInManager,
ILogger<HomeController> logger,
EnterprisePortalCurrentContext enterprisePortalCurrentContext)
{
_signInManager = signInManager;
_logger = logger;
_enterprisePortalCurrentContext = enterprisePortalCurrentContext;
}
public IActionResult Index()
{
if(_signInManager.IsSignedIn(User))
{
return View();
}
else
{
return NotFound();
}
}
[HttpGet("~/alive")]
[HttpGet("~/now")]
[AllowAnonymous]
public DateTime GetAlive()
{
return DateTime.UtcNow;
}
[Authorize]
public IActionResult SetSelectedOrganization(Guid id, string returnUrl)
{
if (_enterprisePortalCurrentContext.Organizations.Any(o => o.Id == id))
{
Response.Cookies.Append("SelectedOrganization", id.ToString(), new CookieOptions { HttpOnly = true });
}
if (!string.IsNullOrWhiteSpace(returnUrl) && Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index");
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

View File

@ -0,0 +1,123 @@
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Portal.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Portal.Controllers
{
[Authorize]
public class PoliciesController : Controller
{
private readonly IUserService _userService;
private readonly IOrganizationService _organizationService;
private readonly IPolicyService _policyService;
private readonly IPolicyRepository _policyRepository;
private readonly EnterprisePortalCurrentContext _enterprisePortalCurrentContext;
private readonly II18nService _i18nService;
public PoliciesController(
IUserService userService,
IOrganizationService organizationService,
IPolicyService policyService,
IPolicyRepository policyRepository,
EnterprisePortalCurrentContext enterprisePortalCurrentContext,
II18nService i18nService)
{
_userService = userService;
_organizationService = organizationService;
_policyService = policyService;
_policyRepository = policyRepository;
_enterprisePortalCurrentContext = enterprisePortalCurrentContext;
_i18nService = i18nService;
}
public async Task<IActionResult> Index()
{
var orgId = _enterprisePortalCurrentContext.SelectedOrganizationId;
if (orgId == null)
{
return Redirect("~/");
}
if (!_enterprisePortalCurrentContext.SelectedOrganizationDetails.UsePolicies ||
!_enterprisePortalCurrentContext.AdminForSelectedOrganization)
{
return Redirect("~/");
}
var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgId.Value);
return View(new PoliciesModel(policies));
}
[HttpGet("/edit/{type}")]
public async Task<IActionResult> Edit(PolicyType type)
{
var orgId = _enterprisePortalCurrentContext.SelectedOrganizationId;
if (orgId == null)
{
return Redirect("~");
}
if (!_enterprisePortalCurrentContext.SelectedOrganizationDetails.UsePolicies ||
!_enterprisePortalCurrentContext.AdminForSelectedOrganization)
{
return Redirect("~/");
}
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId.Value, type);
return BuildPolicyView(policy, type);
}
[HttpPost("/edit/{type}")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(PolicyType type, PolicyEditModel model)
{
var orgId = _enterprisePortalCurrentContext.SelectedOrganizationId;
if (orgId == null)
{
return Redirect("~");
}
if (!_enterprisePortalCurrentContext.SelectedOrganizationDetails.UsePolicies ||
!_enterprisePortalCurrentContext.AdminForSelectedOrganization)
{
return Redirect("~/");
}
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId.Value, type);
if (!ModelState.IsValid)
{
return BuildPolicyView(policy, type);
}
if (policy == null)
{
policy = model.ToPolicy(type, orgId.Value);
}
else
{
policy = model.ToPolicy(policy);
}
var userId = _userService.GetProperUserId(User);
await _policyService.SaveAsync(policy, _userService, _organizationService, userId);
return RedirectToAction("Edit", new { type });
}
private IActionResult BuildPolicyView(Policy policy, PolicyType type)
{
if (policy == null)
{
return View(new PolicyEditModel(type, _i18nService));
}
else
{
return View(new PolicyEditModel(policy, _i18nService));
}
}
}
}

View File

@ -0,0 +1,90 @@
using System.Threading.Tasks;
using Bit.Core;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Portal.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Portal.Controllers
{
[Authorize]
public class SsoController : Controller
{
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly EnterprisePortalCurrentContext _enterprisePortalCurrentContext;
private readonly II18nService _i18nService;
private readonly GlobalSettings _globalSettings;
public SsoController(
ISsoConfigRepository ssoConfigRepository,
EnterprisePortalCurrentContext enterprisePortalCurrentContext,
II18nService i18nService,
GlobalSettings globalSettings)
{
_ssoConfigRepository = ssoConfigRepository;
_enterprisePortalCurrentContext = enterprisePortalCurrentContext;
_i18nService = i18nService;
_globalSettings = globalSettings;
}
[HttpGet]
public async Task<IActionResult> Index()
{
var orgId = _enterprisePortalCurrentContext.SelectedOrganizationId;
if (orgId == null)
{
return Redirect("~/");
}
if (!_enterprisePortalCurrentContext.SelectedOrganizationDetails.UseSso ||
!_enterprisePortalCurrentContext.AdminForSelectedOrganization)
{
return Redirect("~/");
}
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId.Value);
var model = new SsoConfigEditViewModel(ssoConfig, _i18nService, _globalSettings);
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(SsoConfigEditViewModel model)
{
var orgId = _enterprisePortalCurrentContext.SelectedOrganizationId;
if (orgId == null)
{
return Redirect("~/");
}
if (!_enterprisePortalCurrentContext.SelectedOrganizationDetails.UseSso ||
!_enterprisePortalCurrentContext.AdminForSelectedOrganization)
{
return Redirect("~/");
}
model.BuildLists(_i18nService);
if (!ModelState.IsValid)
{
return View(model);
}
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId.Value);
if (ssoConfig == null)
{
ssoConfig = model.ToSsoConfig();
ssoConfig.OrganizationId = orgId.GetValueOrDefault();
await _ssoConfigRepository.CreateAsync(ssoConfig);
}
else
{
ssoConfig = model.ToSsoConfig(ssoConfig);
await _ssoConfigRepository.ReplaceAsync(ssoConfig);
}
return View(model);
}
}
}

View File

@ -0,0 +1,20 @@
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
LABEL com.bitwarden.product="bitwarden"
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
ENV ASPNETCORE_URLS http://+:5000
WORKDIR /app
EXPOSE 5000
COPY obj/Docker/publish .
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -0,0 +1,111 @@
using System;
using Bit.Core;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Bit.Core.Repositories;
using System.Linq;
using System.Collections.Generic;
using Bit.Core.Models.Data;
namespace Bit.Portal
{
public class EnterprisePortalCurrentContext : CurrentContext
{
private readonly IServiceProvider _serviceProvider;
public EnterprisePortalCurrentContext(IServiceProvider serviceProvider)
: base()
{
_serviceProvider = serviceProvider;
}
public Guid? SelectedOrganizationId { get; set; }
public OrganizationUserOrganizationDetails SelectedOrganizationDetails { get; set; }
public List<OrganizationUserOrganizationDetails> OrganizationsDetails { get; set; }
public bool ManagerForSelectedOrganization =>
SelectedOrganizationDetails?.Type == Core.Enums.OrganizationUserType.Manager ||
SelectedOrganizationDetails?.Type == Core.Enums.OrganizationUserType.Admin ||
SelectedOrganizationDetails?.Type == Core.Enums.OrganizationUserType.Owner;
public bool AdminForSelectedOrganization =>
SelectedOrganizationDetails?.Type == Core.Enums.OrganizationUserType.Admin ||
SelectedOrganizationDetails?.Type == Core.Enums.OrganizationUserType.Owner;
public bool OwnerForSelectedOrganization =>
SelectedOrganizationDetails?.Type == Core.Enums.OrganizationUserType.Owner;
public async override Task SetContextAsync(ClaimsPrincipal user)
{
var nameId = user.FindFirstValue(ClaimTypes.NameIdentifier);
if (Guid.TryParse(nameId, out var nameIdGuid))
{
UserId = nameIdGuid;
}
if (!UserId.HasValue)
{
return;
}
// TODO: maybe make loading orgs Lazy<T> somehow?
var orgUserRepo = _serviceProvider.GetRequiredService<IOrganizationUserRepository>();
var userOrgs = await orgUserRepo.GetManyDetailsByUserAsync(UserId.Value);
OrganizationsDetails = userOrgs.ToList();
Organizations = userOrgs.Select(ou => new CurrentContentOrganization
{
Id = ou.OrganizationId,
Type = ou.Type
}).ToList();
if (SelectedOrganizationId == null && HttpContext.Request.Cookies.ContainsKey("SelectedOrganization") &&
Guid.TryParse(HttpContext.Request.Cookies["SelectedOrganization"], out var selectedOrgId))
{
SelectedOrganizationId = Organizations.FirstOrDefault(o => o.Id == selectedOrgId)?.Id;
SelectedOrganizationDetails = OrganizationsDetails.FirstOrDefault(
o => o.OrganizationId == SelectedOrganizationId);
}
if (DeviceIdentifier == null && HttpContext.Request.Cookies.ContainsKey("DeviceIdentifier"))
{
DeviceIdentifier = HttpContext.Request.Cookies["DeviceIdentifier"];
}
DeviceType = Core.Enums.DeviceType.UnknownBrowser;
if (HttpContext.Request.Headers.ContainsKey("User-Agent"))
{
var userAgent = HttpContext.Request.Headers["User-Agent"].ToString();
if (userAgent.Contains(" Firefox/") || userAgent.Contains(" Gecko/"))
{
DeviceType = Core.Enums.DeviceType.FirefoxBrowser;
}
else if (userAgent.IndexOf(" OPR/") >= 0)
{
DeviceType = Core.Enums.DeviceType.OperaBrowser;
}
else if (userAgent.Contains(" Edge/"))
{
DeviceType = Core.Enums.DeviceType.EdgeBrowser;
}
else if (userAgent.Contains(" Vivaldi/"))
{
DeviceType = Core.Enums.DeviceType.VivaldiBrowser;
}
else if (userAgent.Contains(" Safari/") && !userAgent.Contains("Chrome"))
{
DeviceType = Core.Enums.DeviceType.SafariBrowser;
}
else if (userAgent.Contains(" Chrome/"))
{
DeviceType = Core.Enums.DeviceType.ChromeBrowser;
}
else if (userAgent.Contains(" Trident/"))
{
DeviceType = Core.Enums.DeviceType.IEBrowser;
}
}
}
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace Bit.Portal.Models
{
public class ErrorViewModel
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
}

View File

@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Portal.Models
{
public class MasterPasswordDataModel
{
[Display(Name = "MinimumLength")]
[Range(8, int.MaxValue, ErrorMessage = "MasterPasswordMinLengthError")]
public int? MinLength { get; set; }
[Display(Name = "MinimumComplexityScore")]
public int? MinComplexity { get; set; }
[Display(Name = "UppercaseAZ")]
public bool RequireUpper { get; set; }
[Display(Name = "LowercaseAZ")]
public bool RequireLower { get; set; }
[Display(Name = "Numbers09")]
public bool RequireNumbers { get; set; }
[Display(Name = "SpecialCharacters")]
public bool RequireSpecial { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Bit.Portal.Models
{
public class OrganizationPickerViewModel
{
public string SelectedOrganization { get; set; }
public List<SelectListItem> Organizations { get; set; }
}
}

View File

@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Portal.Models
{
public class PasswordGeneratorDataModel
{
// Shared
[Display(Name = "MinimumLength")]
[Range(5, 128)]
public int? MinLength { get; set; }
[Display(Name = "DefaultType")]
public string DefaultType { get; set; }
// PG - Password
[Display(Name = "UppercaseAZ")]
public bool UseUpper { get; set; }
[Display(Name = "LowercaseAZ")]
public bool UseLower { get; set; }
[Display(Name = "Numbers09")]
public bool UseNumbers { get; set; }
[Display(Name = "SpecialCharacters")]
public bool UseSpecial { get; set; }
[Display(Name = "MinimumNumbers")]
[Range(0, 9)]
public int? MinNumbers { get; set; }
[Display(Name = "MinimumSpecial")]
[Range(0, 9)]
public int? MinSpecial { get; set; }
// PG - Passphrase
[Display(Name = "MinimumNumberOfWords")]
[Range(3, 20)]
public int? MinNumberWords { get; set; }
[Display(Name = "Capitalize")]
public bool Capitalize { get; set; }
[Display(Name = "IncludeNumber")]
public bool IncludeNumber { get; set; }
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Enums;
using Bit.Core.Models.Table;
namespace Bit.Portal.Models
{
public class PoliciesModel
{
public PoliciesModel(ICollection<Policy> policies)
{
if (policies == null)
{
return;
}
var policyDict = policies?.ToDictionary(p => p.Type);
Policies = new List<PolicyModel>();
foreach (var type in Enum.GetValues(typeof(PolicyType)).Cast<PolicyType>())
{
var enabled = policyDict?.ContainsKey(type) ?? false && policyDict[type].Enabled;
Policies.Add(new PolicyModel(type, enabled));
}
}
public List<PolicyModel> Policies { get; set; }
}
}

View File

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using Bit.Core.Enums;
using Bit.Core.Models.Table;
using Bit.Core.Services;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Bit.Portal.Models
{
public class PolicyEditModel : PolicyModel
{
public PolicyEditModel() { }
public PolicyEditModel(PolicyType type, II18nService i18nService)
: base(type, false)
{
// Inject service and create static lists
BuildLists(i18nService);
}
public PolicyEditModel(Policy model, II18nService i18nService)
: base(model)
{
if (model == null)
{
return;
}
// Inject service and create static lists
BuildLists(i18nService);
if (model.Data != null)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
switch (model.Type)
{
case PolicyType.MasterPassword:
MasterPasswordDataModel = JsonSerializer.Deserialize<MasterPasswordDataModel>(model.Data, options);
break;
case PolicyType.PasswordGenerator:
PasswordGeneratorDataModel = JsonSerializer.Deserialize<PasswordGeneratorDataModel>(model.Data, options);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
public MasterPasswordDataModel MasterPasswordDataModel { get; set; }
public PasswordGeneratorDataModel PasswordGeneratorDataModel { get; set; }
public List<SelectListItem> Complexities { get; set; }
public List<SelectListItem> DefaultTypes { get; set; }
public Policy ToPolicy(PolicyType type, Guid organizationId)
{
return ToPolicy(new Policy
{
Type = type,
OrganizationId = organizationId
});
}
public Policy ToPolicy(Policy existingPolicy)
{
existingPolicy.Enabled = Enabled;
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
switch (existingPolicy.Type)
{
case PolicyType.MasterPassword:
existingPolicy.Data = JsonSerializer.Serialize(MasterPasswordDataModel, options);
break;
case PolicyType.PasswordGenerator:
existingPolicy.Data = JsonSerializer.Serialize(PasswordGeneratorDataModel, options);
break;
default:
throw new ArgumentOutOfRangeException();
}
return existingPolicy;
}
public void BuildLists(II18nService i18nService)
{
Complexities = new List<SelectListItem>
{
new SelectListItem { Value = null, Text = "--" + i18nService.T("Select") + "--"},
new SelectListItem { Value = "0", Text = i18nService.T("Weak") + " (0)" },
new SelectListItem { Value = "1", Text = i18nService.T("Weak") + " (1)" },
new SelectListItem { Value = "2", Text = i18nService.T("Weak") + " (2)" },
new SelectListItem { Value = "3", Text = i18nService.T("Good") + " (3)" },
new SelectListItem { Value = "4", Text = i18nService.T("Strong") + " (4)" },
};
DefaultTypes = new List<SelectListItem>
{
new SelectListItem { Value = null, Text = i18nService.T("UserPreference") },
new SelectListItem { Value = "password", Text = i18nService.T("Password") },
new SelectListItem { Value = "passphrase", Text = i18nService.T("Passphrase") },
};
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.Models.Table;
namespace Bit.Portal.Models
{
public class PolicyModel
{
public PolicyModel() { }
public PolicyModel(Policy policy)
: this(policy.Type, policy.Enabled)
{ }
public PolicyModel(PolicyType policyType, bool enabled)
{
switch (policyType)
{
case PolicyType.TwoFactorAuthentication:
NameKey = "TwoStepLogin";
DescriptionKey = "TwoStepLoginDescription";
break;
case PolicyType.MasterPassword:
NameKey = "MasterPassword";
DescriptionKey = "MasterPasswordDescription";
break;
case PolicyType.PasswordGenerator:
NameKey = "PasswordGenerator";
DescriptionKey = "PasswordGeneratorDescription";
break;
default:
throw new ArgumentOutOfRangeException();
}
PolicyType = policyType;
Enabled = enabled;
}
public string NameKey { get; set; }
public string DescriptionKey { get; set; }
public PolicyType PolicyType { get; set; }
[Display(Name = "Enabled")]
public bool Enabled { get; set; }
}
}

View File

@ -0,0 +1,175 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using Bit.Core.Services;
using Bit.Core;
using Bit.Core.Models.Data;
using Bit.Core.Enums;
using Bit.Core.Sso;
namespace Bit.Portal.Models
{
public class SsoConfigDataViewModel : IValidatableObject
{
public SsoConfigDataViewModel() { }
public SsoConfigDataViewModel(SsoConfigurationData configurationData, GlobalSettings globalSettings)
{
ConfigType = configurationData.ConfigType;
Authority = configurationData.Authority;
ClientId = configurationData.ClientId;
ClientSecret = configurationData.ClientSecret;
CallbackPath = configurationData.BuildCallbackPath(globalSettings.BaseServiceUri.Sso);
SignedOutCallbackPath = configurationData.BuildSignedOutCallbackPath(globalSettings.BaseServiceUri.Sso);
MetadataAddress = configurationData.MetadataAddress;
GetClaimsFromUserInfoEndpoint = configurationData.GetClaimsFromUserInfoEndpoint;
SpEntityId = configurationData.BuildSaml2ModulePath(globalSettings.BaseServiceUri.Sso);
IdpEntityId = configurationData.IdpEntityId;
IdpBindingType = configurationData.IdpBindingType;
IdpSingleSignOnServiceUrl = configurationData.IdpSingleSignOnServiceUrl;
IdpSingleLogoutServiceUrl = configurationData.IdpSingleLogoutServiceUrl;
IdpArtifactResolutionServiceUrl = configurationData.IdpArtifactResolutionServiceUrl;
IdpX509PublicCert = configurationData.IdpX509PublicCert;
IdpOutboundSigningAlgorithm = configurationData.IdpOutboundSigningAlgorithm;
IdpAllowUnsolicitedAuthnResponse = configurationData.IdpAllowUnsolicitedAuthnResponse;
IdpDisableOutboundLogoutRequests = configurationData.IdpDisableOutboundLogoutRequests;
IdpWantAuthnRequestsSigned = configurationData.IdpWantAuthnRequestsSigned;
SpNameIdFormat = configurationData.SpNameIdFormat;
SpOutboundSigningAlgorithm = configurationData.SpOutboundSigningAlgorithm ?? SamlSigningAlgorithms.Sha256;
SpSigningBehavior = configurationData.SpSigningBehavior;
SpWantAssertionsSigned = configurationData.SpWantAssertionsSigned;
SpValidateCertificates = configurationData.SpValidateCertificates;
}
[Required]
[Display(Name = "ConfigType")]
public SsoType ConfigType { get; set; }
// OIDC
[Display(Name = "Authority")]
public string Authority { get; set; }
[Display(Name = "ClientId")]
public string ClientId { get; set; }
[Display(Name = "ClientSecret")]
public string ClientSecret { get; set; }
[Display(Name = "CallbackPath")]
public string CallbackPath { get; set; }
[Display(Name = "SignedOutCallbackPath")]
public string SignedOutCallbackPath { get; set; }
[Display(Name = "MetadataAddress")]
public string MetadataAddress { get; set; }
[Display(Name = "GetClaimsFromUserInfoEndpoint")]
public bool GetClaimsFromUserInfoEndpoint { get; set; }
// SAML2 SP
[Display(Name = "SpEntityId")]
public string SpEntityId { get; set; }
[Display(Name = "NameIdFormat")]
public Saml2NameIdFormat SpNameIdFormat { get; set; }
[Display(Name = "OutboundSigningAlgorithm")]
public string SpOutboundSigningAlgorithm { get; set; }
[Display(Name = "SigningBehavior")]
public Saml2SigningBehavior SpSigningBehavior { get; set; }
[Display(Name = "SpWantAssertionsSigned")]
public bool SpWantAssertionsSigned { get; set; }
[Display(Name = "SpValidateCertificates")]
public bool SpValidateCertificates { get; set; }
// SAML2 IDP
[Display(Name = "EntityId")]
public string IdpEntityId { get; set; }
[Display(Name = "BindingType")]
public Saml2BindingType IdpBindingType { get; set; }
[Display(Name = "SingleSignOnServiceUrl")]
public string IdpSingleSignOnServiceUrl { get; set; }
[Display(Name = "SingleLogoutServiceUrl")]
public string IdpSingleLogoutServiceUrl { get; set; }
[Display(Name = "ArtifactResolutionServiceUrl")]
public string IdpArtifactResolutionServiceUrl { get; set; }
[Display(Name = "X509PublicCert")]
public string IdpX509PublicCert { get; set; }
[Display(Name = "OutboundSigningAlgorithm")]
public string IdpOutboundSigningAlgorithm { get; set; }
[Display(Name = "AllowUnsolicitedAuthnResponse")]
public bool IdpAllowUnsolicitedAuthnResponse { get; set; }
[Display(Name = "DisableOutboundLogoutRequests")]
public bool IdpDisableOutboundLogoutRequests { get; set; }
[Display(Name = "WantAuthnRequestsSigned")]
public bool IdpWantAuthnRequestsSigned { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
var i18nService = context.GetService(typeof(II18nService)) as I18nService;
var model = context.ObjectInstance as SsoConfigDataViewModel;
if (model.ConfigType == SsoType.OpenIdConnect)
{
if (string.IsNullOrWhiteSpace(model.Authority))
{
yield return new ValidationResult(i18nService.GetLocalizedHtmlString("AuthorityValidationError"),
new[] { nameof(model.Authority) });
}
if (string.IsNullOrWhiteSpace(model.ClientId))
{
yield return new ValidationResult(i18nService.GetLocalizedHtmlString("ClientIdValidationError"),
new[] { nameof(model.ClientId) });
}
if (string.IsNullOrWhiteSpace(model.ClientSecret))
{
yield return new ValidationResult(i18nService.GetLocalizedHtmlString("ClientSecretValidationError"),
new[] { nameof(model.ClientSecret) });
}
}
else if (model.ConfigType == SsoType.Saml2)
{
if (string.IsNullOrWhiteSpace(model.IdpEntityId))
{
yield return new ValidationResult(i18nService.GetLocalizedHtmlString("IdpEntityIdValidationError"),
new[] { nameof(model.IdpEntityId) });
}
if (model.IdpBindingType == Saml2BindingType.Artifact && string.IsNullOrWhiteSpace(model.IdpArtifactResolutionServiceUrl))
{
yield return new ValidationResult(i18nService.GetLocalizedHtmlString("Saml2BindingTypeValidationError"),
new[] { nameof(model.IdpArtifactResolutionServiceUrl) });
}
if (!Uri.IsWellFormedUriString(model.IdpEntityId, UriKind.Absolute) && string.IsNullOrWhiteSpace(model.IdpSingleSignOnServiceUrl))
{
yield return new ValidationResult(i18nService.GetLocalizedHtmlString("IdpSingleSignOnServiceUrlValidationError"),
new[] { nameof(model.IdpSingleSignOnServiceUrl) });
}
}
}
public SsoConfigurationData ToConfigurationData()
{
return new SsoConfigurationData
{
ConfigType = ConfigType,
Authority = Authority,
ClientId = ClientId,
ClientSecret = ClientSecret,
MetadataAddress = MetadataAddress,
GetClaimsFromUserInfoEndpoint = GetClaimsFromUserInfoEndpoint,
IdpEntityId = IdpEntityId,
IdpBindingType = IdpBindingType,
IdpSingleSignOnServiceUrl = IdpSingleSignOnServiceUrl,
IdpSingleLogoutServiceUrl = IdpSingleLogoutServiceUrl,
IdpArtifactResolutionServiceUrl = IdpArtifactResolutionServiceUrl,
IdpX509PublicCert = IdpX509PublicCert,
IdpOutboundSigningAlgorithm = IdpOutboundSigningAlgorithm,
IdpAllowUnsolicitedAuthnResponse = IdpAllowUnsolicitedAuthnResponse,
IdpDisableOutboundLogoutRequests = IdpDisableOutboundLogoutRequests,
IdpWantAuthnRequestsSigned = IdpWantAuthnRequestsSigned,
SpNameIdFormat = SpNameIdFormat,
SpOutboundSigningAlgorithm = SpOutboundSigningAlgorithm ?? SamlSigningAlgorithms.Sha256,
SpSigningBehavior = SpSigningBehavior,
SpWantAssertionsSigned = SpWantAssertionsSigned,
SpValidateCertificates = SpValidateCertificates,
};
}
}
}

View File

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.Json;
using Bit.Core;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using Bit.Core.Services;
using Bit.Core.Sso;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Bit.Portal.Models
{
public class SsoConfigEditViewModel
{
public SsoConfigEditViewModel() { }
public SsoConfigEditViewModel(SsoConfig ssoConfig, II18nService i18nService,
GlobalSettings globalSettings)
{
if (ssoConfig != null)
{
Id = ssoConfig.Id;
Enabled = ssoConfig.Enabled;
}
SsoConfigurationData configurationData;
if (!string.IsNullOrWhiteSpace(ssoConfig?.Data))
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
configurationData = JsonSerializer.Deserialize<SsoConfigurationData>(ssoConfig.Data, options);
}
else
{
configurationData = new SsoConfigurationData();
}
Data = new SsoConfigDataViewModel(configurationData, globalSettings);
BuildLists(i18nService);
}
public long Id { get; set; }
[Display(Name = "Enabled")]
public bool Enabled { get; set; }
public SsoConfigDataViewModel Data { get; set; }
public List<SelectListItem> ConfigTypes { get; set; }
public List<SelectListItem> SpNameIdFormats { get; set; }
public List<SelectListItem> BindingTypes { get; set; }
public List<SelectListItem> SigningBehaviors { get; set; }
public List<SelectListItem> SigningAlgorithms { get; set; }
public SsoConfig ToSsoConfig()
{
return ToSsoConfig(new SsoConfig());
}
public SsoConfig ToSsoConfig(SsoConfig existingConfig)
{
existingConfig.Enabled = Enabled;
var configurationData = Data.ToConfigurationData();
existingConfig.Data = JsonSerializer.Serialize(configurationData, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
return existingConfig;
}
public void BuildLists(II18nService i18nService)
{
ConfigTypes = Enum.GetNames(typeof(SsoType))
.Select(configType => new SelectListItem
{
Value = configType,
Text = i18nService.T(configType),
}).ToList();
SpNameIdFormats = Enum.GetNames(typeof(Saml2NameIdFormat))
.Select(nameIdFormat => new SelectListItem
{
Value = nameIdFormat,
Text = i18nService.T(nameIdFormat),
}).ToList();
BindingTypes = Enum.GetNames(typeof(Saml2BindingType))
.Select(bindingType => new SelectListItem
{
Value = bindingType,
Text = i18nService.T(bindingType),
}).ToList();
SigningBehaviors = Enum.GetNames(typeof(Saml2SigningBehavior))
.Select(behavior => new SelectListItem
{
Value = behavior,
Text = i18nService.T(behavior),
}).ToList();
SigningAlgorithms = SamlSigningAlgorithms.GetEnumerable().Select(a =>
new SelectListItem(a, a)).ToList();
}
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<Version>1.33.1</Version>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>Bit.Portal</RootNamespace>
<UserSecretsId>bitwarden-Portal</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Core\Core.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
using System;
namespace Bit.Portal
{
public class PortalSettings
{
}
}

View File

@ -0,0 +1,35 @@
using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Bit.Core.Utilities;
using Serilog.Events;
namespace Bit.Portal
{
public class Program
{
public static void Main(string[] args)
{
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureLogging((hostingContext, logging) =>
logging.AddSerilog(hostingContext, e =>
{
var context = e.Properties["SourceContext"].ToString();
if (e.Properties.ContainsKey("RequestPath") &&
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
{
return false;
}
return e.Level >= LogEventLevel.Error;
}));
})
.Build()
.Run();
}
}
}

View File

@ -0,0 +1,5 @@
using System.Reflection;
using Microsoft.Extensions.Localization;
[assembly: ResourceLocation("Resources")]
[assembly: RootNamespace("Bit.Portal")]

View File

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:52313",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Portal": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:52313",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,226 @@
$primary: #175DDC;
$primary-accent: #1252A3;
$success: #00a65a;
$info: #555555;
$warning: #bf7e16;
$danger: #dd4b39;
$theme-colors: ( "primary-accent": $primary-accent );
$body-bg: #ffffff;
$body-color: #333333;
$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica, Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol';
$h1-font-size: 2rem;
$h2-font-size: 1.3rem;
$h3-font-size: 1rem;
$h4-font-size: 1rem;
$h5-font-size: 1rem;
$h6-font-size: 1rem;
$small-font-size: 90%;
$font-size-lg: 1.15rem;
$code-font-size: 100%;
$navbar-padding-y: .75rem;
$grid-gutter-width: 20px;
$card-spacer-y: .6rem;
$list-group-item-padding-y: .6rem;
$list-group-active-color: $body-color;
$list-group-active-bg: #ffffff;
$list-group-active-border-color: rgba(#000000, .125);
$dropdown-link-color: $body-color;
$dropdown-link-hover-bg: rgba(#000000, .06);
$dropdown-link-active-color: $dropdown-link-color;
$dropdown-link-active-bg: rgba(#000000, .1);
$dropdown-item-padding-x: 1rem;
$navbar-brand-font-size: 35px;
$navbar-brand-height: 35px;
$navbar-brand-padding-y: 0;
$navbar-dark-color: rgba(#ffffff, .7);
$navbar-dark-hover-color: rgba(#ffffff, .9);
$navbar-nav-link-padding-x: 0.8rem;
$input-bg: #fbfbfb;
$input-focus-bg: #ffffff;
$input-disabled-bg: #e0e0e0;
$input-placeholder-color: #b4b4b4;
$table-accent-bg: rgba(#000000, .02);
$table-hover-bg: rgba(#000000, .03);
$modal-backdrop-opacity: 0.3;
$btn-font-weight: 600;
$lead-font-weight: normal;
$grid-breakpoints: (
xs: 0,
sm: 1px,
md: 2px,
lg: 3px,
xl: 4px
);
@import "../node_modules/bootstrap/scss/bootstrap.scss";
html {
font-size: 14px;
}
body {
min-width: 1010px;
}
.page-header, .secondary-header {
border-bottom: 1px solid $border-color;
padding-bottom: 0.6rem;
margin-bottom: 0.5rem;
h1, h2, h3, h4 {
margin: 0;
}
}
h1, h2, h3, h4, h5 {
small {
font-size: 80%;
}
}
input, select, textarea {
&:required {
box-shadow: none;
}
}
.navbar {
padding-left: 0;
padding-right: 0;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
.dropdown-menu {
min-width: 200px;
max-width: 300px;
.dropdown-item-text {
line-height: 1.3;
span, small {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.nav-link {
font-weight: 600;
}
}
.dropdown-menu {
button {
cursor: pointer;
}
}
.container {
width: 980px;
max-width: none !important;
margin: 0 auto;
padding: 0;
}
.page-content {
margin-top: 20px;
}
.footer {
margin-top: 40px;
padding: 40px 0 40px 0;
border-top: 1px solid $border-color;
}
.callout {
padding: $alert-padding-y $alert-padding-x;
margin-bottom: $alert-margin-bottom;
border: 1px solid $card-border-color;
border-left-width: 5px;
border-radius: $card-inner-border-radius;
background-color: #fafafa;
.callout-heading {
margin-top: 0;
}
h3.callout-heading {
font-weight: bold;
text-transform: uppercase;
}
&.callout-primary {
border-left-color: $primary;
.callout-heading {
color: $primary;
}
}
&.callout-info {
border-left-color: $gray-800;
.callout-heading {
color: $gray-800;
}
}
&.callout-danger {
border-left-color: $danger;
.callout-heading {
color: $danger;
}
}
&.callout-success {
border-left-color: $success;
.callout-heading {
color: $success;
}
}
&.callout-warning {
border-left-color: $warning;
.callout-heading {
color: $warning;
}
}
}
.config-section {
padding-top: 20px;
padding-bottom: 20px;
h2 {
border-bottom: 1px solid #ccc;
}
}
//////////////////////////
.validation-summary-valid {
display: none;
}
.alert.validation-summary-errors > ul {
margin-bottom: 0;
}

View File

@ -0,0 +1,110 @@
using Bit.Core;
using Bit.Core.Utilities;
using Bit.Portal.Utilities;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Bit.Portal
{
public class Startup
{
public Startup(IWebHostEnvironment env, IConfiguration configuration)
{
Configuration = configuration;
Environment = env;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment Environment { get; set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Options
services.AddOptions();
// Settings
var globalSettings = services.AddGlobalSettingsServices(Configuration);
services.Configure<PortalSettings>(Configuration.GetSection("PortalSettings"));
// Data Protection
services.AddCustomDataProtectionServices(Environment, globalSettings);
// Repositories
services.AddSqlServerRepositories(globalSettings);
// Context
services.AddScoped<EnterprisePortalCurrentContext>();
services.AddScoped<CurrentContext>((serviceProvider) =>
serviceProvider.GetService<EnterprisePortalCurrentContext>());
// Identity
services.AddEnterprisePortalTokenIdentityServices();
if (globalSettings.SelfHosted)
{
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Path = "/portal";
});
}
// Services
services.AddBaseServices();
services.AddDefaultServices(globalSettings);
services.AddCoreLocalizationServices();
// Mvc
services.AddControllersWithViews()
.AddViewAndDataAnnotationLocalization();
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IHostApplicationLifetime appLifetime,
GlobalSettings globalSettings,
ILogger<Startup> logger)
{
app.UseSerilog(env, appLifetime, globalSettings);
if (globalSettings.SelfHosted)
{
app.UsePathBase("/portal");
app.UseForwardedHeaders(globalSettings);
}
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCoreLocalization();
// Add static files to the request pipeline.
app.UseStaticFiles();
// Add routing
app.UseRouting();
// Add authentication and authorization to the request pipeline.
app.UseAuthentication();
app.UseAuthorization();
// Add current context
app.UseMiddleware<EnterprisePortalCurrentContextMiddleware>();
// Add endpoints to the request pipeline.
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
// Log startup
logger.LogInformation(Constants.BypassFiltersEventId, globalSettings.ProjectName + " started.");
}
}
}

View File

@ -0,0 +1,23 @@
using Bit.Core;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
namespace Bit.Portal.Utilities
{
public class EnterprisePortalCurrentContextMiddleware
{
private readonly RequestDelegate _next;
public EnterprisePortalCurrentContextMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext, EnterprisePortalCurrentContext currentContext,
GlobalSettings globalSettings)
{
await currentContext.BuildAsync(httpContext, globalSettings);
await _next.Invoke(httpContext);
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using Bit.Core.Identity;
using Bit.Core.Models.Table;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Bit.Portal.Utilities
{
public static class EnterprisePortalServiceCollectionExtensions
{
public static (IdentityBuilder, IdentityBuilder) AddEnterprisePortalTokenIdentityServices(
this IServiceCollection services)
{
services.TryAddTransient<ILookupNormalizer, LowerInvariantLookupNormalizer>();
var passwordlessIdentityBuilder = services.AddIdentity<User, Role>()
.AddUserStore<UserStore>()
.AddRoleStore<RoleStore>()
.AddDefaultTokenProviders();
var regularIdentityBuilder = services.AddIdentityCore<User>()
.AddUserStore<UserStore>();
services.TryAddScoped<EnterprisePortalTokenSignInManager, EnterprisePortalTokenSignInManager>();
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.AccessDeniedPath = "/access-denied";
options.Cookie.Name = $"Bitwarden_BusinessPortal";
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromDays(2);
options.ReturnUrlParameter = "returnUrl";
options.SlidingExpiration = true;
});
return (passwordlessIdentityBuilder, regularIdentityBuilder);
}
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Models.Table;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Bit.Portal.Utilities
{
public class EnterprisePortalTokenSignInManager : SignInManager<User>
{
public const string TokenSignInPurpose = "EnterprisePortalTokenSignIn";
public EnterprisePortalTokenSignInManager(
UserManager<User> userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<User> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<User>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<User> confirmation)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{ }
public async Task<SignInResult> TokenSignInAsync(User user, string token, bool isPersistent)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var attempt = await CheckTokenSignInAsync(user, token);
return attempt.Succeeded ?
await SignInOrTwoFactorAsync(user, isPersistent, bypassTwoFactor: true) : attempt;
}
public async Task<SignInResult> TokenSignInAsync(string userId, string token, bool isPersistent)
{
var user = await UserManager.FindByIdAsync(userId);
if (user == null)
{
return SignInResult.Failed;
}
return await TokenSignInAsync(user, token, isPersistent);
}
public virtual async Task<SignInResult> CheckTokenSignInAsync(User user, string token)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var error = await PreSignInCheck(user);
if (error != null)
{
return error;
}
if (await UserManager.VerifyUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
TokenSignInPurpose, token))
{
return SignInResult.Success;
}
Logger.LogWarning(2, "User {userId} failed to provide the correct enterprise portal token.",
await UserManager.GetUserIdAsync(user));
return SignInResult.Failed;
}
}
}

View File

@ -0,0 +1,8 @@
@inject Bit.Core.Services.II18nService i18nService
@{
ViewData["Title"] = i18nService.T("AccessDenied");
}
<p>
@i18nService.T("AccessDeniedError")
</p>

View File

@ -0,0 +1,8 @@
@inject Bit.Core.Services.II18nService i18nService
@{
ViewData["Title"] = i18nService.T("LoggedOut");
}
<p>
@i18nService.T("LoggedOutMessage")
</p>

View File

@ -0,0 +1,8 @@
@{
ViewData["Title"] = "Home Page";
}
<div class="page-header">
<h1>Welcome</h1>
</div>
<p>Hello world...</p>

View File

@ -0,0 +1,141 @@
@using Bit.Core.Enums
@model PolicyEditModel
@inject Bit.Core.Services.II18nService i18nService
@{
ViewData["Title"] = i18nService.T("EditPolicy", i18nService.T(Model.NameKey));
}
<div class="page-header">
<h1>@i18nService.T("EditPolicy", i18nService.T(Model.NameKey))</h1>
</div>
<p>@i18nService.T(Model.DescriptionKey)</p>
<form method="post" id="edit-form">
@if (Model.PolicyType == PolicyType.TwoFactorAuthentication)
{
<div class="callout callout-warning" role="alert">
<h3 class="callout-heading">
<i class="fa fa-warning" *ngIf="icon" aria-hidden="true"></i>
@i18nService.T("Warning")
</h3>
@i18nService.T("EditPolicyTwoStepLoginWarning")
</div>
}
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="Enabled">
<label class="form-check-label" asp-for="Enabled"></label>
</div>
</div>
@if (Model.PolicyType == PolicyType.MasterPassword)
{
<div class="row">
<div class="col-6 form-group">
<label asp-for="MasterPasswordDataModel.MinComplexity"></label>
<select asp-for="MasterPasswordDataModel.MinComplexity" asp-items="Model.Complexities"
class="form-control"></select>
</div>
<div class="col-6 form-group">
<label asp-for="MasterPasswordDataModel.MinLength"></label>
<input class="form-control" type="number" asp-for="MasterPasswordDataModel.MinLength">
<span asp-validation-for="MasterPasswordDataModel.MinLength" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="MasterPasswordDataModel.RequireUpper">
<label class="form-check-label" asp-for="MasterPasswordDataModel.RequireUpper"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="MasterPasswordDataModel.RequireLower">
<label class="form-check-label" asp-for="MasterPasswordDataModel.RequireLower"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="MasterPasswordDataModel.RequireNumbers">
<label class="form-check-label" asp-for="MasterPasswordDataModel.RequireNumbers"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="MasterPasswordDataModel.RequireSpecial">
<label class="form-check-label" asp-for="MasterPasswordDataModel.RequireSpecial"></label>
</div>
</div>
}
@if (Model.PolicyType == PolicyType.PasswordGenerator)
{
<div class="row">
<div class="col-6 form-group mb-0">
<label asp-for="PasswordGeneratorDataModel.DefaultType"></label>
<select asp-for="PasswordGeneratorDataModel.DefaultType" asp-items="Model.DefaultTypes"
class="form-control"></select>
</div>
</div>
<div class="config-section">
<h2>@i18nService.T("Password")</h2>
<div class="row">
<div class="col-6 form-group">
<label asp-for="PasswordGeneratorDataModel.MinLength"></label>
<input class="form-control" type="number" asp-for="PasswordGeneratorDataModel.MinLength">
<span asp-validation-for="PasswordGeneratorDataModel.MinLength" class="text-danger"></span>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="PasswordGeneratorDataModel.MinNumbers"></label>
<input class="form-control" type="number" asp-for="PasswordGeneratorDataModel.MinNumbers">
<span asp-validation-for="PasswordGeneratorDataModel.MinNumbers" class="text-danger"></span>
</div>
<div class="col-6 form-group">
<label asp-for="PasswordGeneratorDataModel.MinSpecial"></label>
<input class="form-control" type="number" asp-for="PasswordGeneratorDataModel.MinSpecial">
<span asp-validation-for="PasswordGeneratorDataModel.MinSpecial" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="PasswordGeneratorDataModel.UseUpper">
<label class="form-check-label" asp-for="PasswordGeneratorDataModel.UseUpper"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="PasswordGeneratorDataModel.UseLower">
<label class="form-check-label" asp-for="PasswordGeneratorDataModel.UseLower"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="PasswordGeneratorDataModel.UseNumbers">
<label class="form-check-label" asp-for="PasswordGeneratorDataModel.UseNumbers"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="PasswordGeneratorDataModel.UseSpecial">
<label class="form-check-label" asp-for="PasswordGeneratorDataModel.UseSpecial"></label>
</div>
</div>
</div>
<div class="config-section">
<h2>@i18nService.T("Passphrase")</h2>
<div class="row">
<div class="col-6 form-group">
<label asp-for="PasswordGeneratorDataModel.MinNumberWords"></label>
<input class="form-control" type="number" asp-for="PasswordGeneratorDataModel.MinNumberWords">
<span asp-validation-for="PasswordGeneratorDataModel.MinNumberWords" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="PasswordGeneratorDataModel.Capitalize">
<label class="form-check-label" asp-for="PasswordGeneratorDataModel.Capitalize"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="PasswordGeneratorDataModel.IncludeNumber">
<label class="form-check-label" asp-for="PasswordGeneratorDataModel.IncludeNumber"></label>
</div>
</div>
</div>
}
<div class="d-flex">
<button type="submit" class="btn btn-primary">@i18nService.T("Save")</button>
<a class="btn btn-outline-secondary ml-1" asp-controller="Policies" asp-action="Index">@i18nService.T("Cancel")</a>
</div>
</form>

View File

@ -0,0 +1,29 @@
@model PoliciesModel
@inject Bit.Core.Services.II18nService i18nService
@{
ViewData["Title"] = i18nService.T("Policies");
}
<div class="page-header">
<h1>@i18nService.T("Policies")</h1>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<tbody>
@foreach (var policyModel in Model.Policies)
{
<tr>
<td>
<a asp-action="Edit" asp-route-type="@policyModel.PolicyType">@i18nService.T(policyModel.NameKey)</a>
@if (policyModel.Enabled)
{
<span class="badge badge-success">@i18nService.T("Enabled")</span>
}
<small class="text-muted d-block">@i18nService.T(policyModel.DescriptionKey)</small>
</td>
</tr>
}
</tbody>
</table>
</div>

View File

@ -0,0 +1,9 @@
@model OrganizationPickerViewModel
<form method="get" asp-action="SetSelectedOrganization" asp-controller="Home" class="form-inline my-2 my-lg-0">
<label asp-for="SelectedOrganization" class="sr-only">Organization</label>
<select asp-for="SelectedOrganization" asp-items="Model.Organizations" name="id" class="form-control mr-sm-2">
<option value="">--Select Organization--</option>
</select>
<button type="submit" class="btn btn-outline-light my-2 my-sm-0">Go</button>
</form>

View File

@ -0,0 +1,25 @@
@model ErrorViewModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>

View File

@ -0,0 +1,121 @@
@using static Bit.Core.Utilities.CoreHelpers;
@inject SignInManager<Bit.Core.Models.Table.User> SignInManager
@inject Bit.Core.Services.II18nService i18nService
@inject Bit.Portal.EnterprisePortalCurrentContext EnterprisePortalCurrentContext
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Business Portal</title>
<link rel="stylesheet" href="~/css/webfonts.css" />
<environment include="Development">
<link rel="stylesheet" href="~/lib/font-awesome/css/font-awesome.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="~/lib/font-awesome/css/font-awesome.min.css" asp-append-version="true" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-primary mb-4">
<div class="container">
<a class="navbar-brand" asp-controller="Home" asp-action="Index">
<i class="fa fa-shield" aria-hidden="true"></i>
</a>
@if (SignInManager.IsSignedIn(User))
{
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto">
@if (EnterprisePortalCurrentContext.SelectedOrganizationDetails.UseSso &&
EnterprisePortalCurrentContext.AdminForSelectedOrganization)
{
<li class="nav-item">
<a class="nav-link" asp-area="" asp-controller="Sso" asp-action="Index">
@i18nService.T("SingleSignOn")
</a>
</li>
}
@if (EnterprisePortalCurrentContext.SelectedOrganizationDetails.UsePolicies &&
EnterprisePortalCurrentContext.AdminForSelectedOrganization)
{
<li class="nav-item">
<a class="nav-link" asp-area="" asp-controller="Policies" asp-action="Index">
@i18nService.T("Policies")
</a>
</li>
}
</ul>
</div>
}
@if (SignInManager.IsSignedIn(User))
{
@await Component.InvokeAsync("OrganizationPicker")
<ul class="navbar-nav flex-row ml-md-auto d-none d-md-flex">
<li class="nav-item dropdown">
<a class="nav-item nav-link dropdown-toggle" href="#" id="nav-profile" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fa fa-user-circle fa-lg" aria-hidden="true"></i>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="nav-profile">
<div class="dropdown-item-text d-flex align-items-center">
<i aria-hidden="true" class="fa fa-user-circle fa-lg"></i>
<div class="ml-2 overflow-hidden">
<span>Logged in as</span>
<small class="text-muted">@User.Identity.Name</small>
</div>
</div>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#">
<i class="fa fa-fw fa-share fa-flip-horizontal" aria-hidden="true"></i>
Return to My Vault
</a>
<form asp-controller="Auth" asp-action="Logout" method="post">
<button type="submit" class="dropdown-item">
<i class="fa fa-fw fa-sign-out" aria-hidden="true"></i>
Log Out
</button>
</form>
</div>
</li>
</ul>
}
</div>
</nav>
<div class="container page-content">
@RenderBody()
</div>
<div class="container footer text-muted">
<div class="row">
<div class="col">
&copy; @DateTime.Now.Year, Bitwarden Inc.
</div>
<div class="col text-center"></div>
<div class="col text-right">
Version @GetVersion()
</div>
</div>
</div>
<environment include="Development">
<script src="~/lib/jquery/jquery.slim.js"></script>
<script src="~/lib/popper/popper.js"></script>
<script src="~/lib/bootstrap/js/bootstrap.js"></script>
</environment>
<environment exclude="Development">
<script src="~/lib/jquery/jquery.slim.min.js" asp-append-version="true"></script>
<script src="~/lib/popper/popper.min.js" asp-append-version="true"></script>
<script src="~/lib/bootstrap/js/bootstrap.min.js" asp-append-version="true"></script>
</environment>
<script src="~/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
</body>
</html>

View File

@ -0,0 +1,226 @@
@inject Bit.Core.Services.II18nService i18nService
@model SsoConfigEditViewModel
@{
ViewData["Title"] = i18nService.T("EditSsoConfig");
}
@section Scripts {
<script type="text/javascript">
function toggleVisibility() {
var value = $('#Data_ConfigType').val();
if (value == 'OpenIdConnect') {
$('.oidc-config').show();
$('.saml-config').hide();
} else {
$('.oidc-config').hide();
$('.saml-config').show();
}
}
$(function () {
// Set initial visibility
toggleVisibility();
// Toggle visibiity on change
$('#Data_ConfigType').change(function () {
toggleVisibility();
});
});
</script>
}
<div class="page-header">
<h1>@i18nService.T("SingleSignOn")</h1>
</div>
<form method="post" id="edit-form">
<div class="form-group">
<div class="form-check">
<input asp-for="Enabled" type="checkbox" class="form-check-input">
<label asp-for="Enabled" class="form-check-label"></label>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.ConfigType"></label>
<select asp-for="Data.ConfigType" asp-items="Model.ConfigTypes" class="form-control"></select>
</div>
</div>
<!-- OIDC -->
<div class="oidc-config">
<div class="config-section">
<h2>@i18nService.T("OpenIdConnectConfig")</h2>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.CallbackPath">@i18nService.T("CallbackPath")</label>
<input asp-for="Data.CallbackPath" class="form-control" readonly>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.SignedOutCallbackPath">@i18nService.T("SignedOutCallbackPath")</label>
<input asp-for="Data.SignedOutCallbackPath" class="form-control" readonly>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.Authority">@i18nService.T("Authority")</label>
<input asp-for="Data.Authority" class="form-control">
<span asp-validation-for="Data.Authority" class="text-danger"></span>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.ClientId">@i18nService.T("ClientId")</label>
<input asp-for="Data.ClientId" class="form-control">
<span asp-validation-for="Data.ClientId" class="text-danger"></span>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.ClientSecret">@i18nService.T("ClientSecret")</label>
<input asp-for="Data.ClientSecret" class="form-control">
<span asp-validation-for="Data.ClientSecret" class="text-danger"></span>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.MetadataAddress">@i18nService.T("MetadataAddress")</label>
<input asp-for="Data.MetadataAddress" class="form-control">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<div class="form-check">
<input asp-for="Data.GetClaimsFromUserInfoEndpoint" type="checkbox" class="form-check-input">
<label asp-for="Data.GetClaimsFromUserInfoEndpoint" class="form-check-label"></label>
</div>
</div>
</div>
</div>
</div>
<div class="saml-config">
<!-- SAML2 SP -->
<div class="config-section">
<h2>@i18nService.T("SamlSpConfig")</h2>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.SpEntityId">@i18nService.T("SpEntityId")</label>
<input asp-for="Data.SpEntityId" class="form-control" readonly>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.SpNameIdFormat">@i18nService.T("NameIdFormat")</label>
<select asp-for="Data.SpNameIdFormat" asp-items="Model.SpNameIdFormats"
class="form-control"></select>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.SpOutboundSigningAlgorithm">@i18nService.T("OutboundSigningAlgorithm")</label>
<select asp-for="Data.SpOutboundSigningAlgorithm" asp-items="Model.SigningAlgorithms"
class="form-control"></select>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.SpSigningBehavior">@i18nService.T("SigningBehavior")</label>
<select asp-for="Data.SpSigningBehavior" asp-items="Model.SigningBehaviors"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="Data.SpWantAssertionsSigned" type="checkbox" class="form-check-input">
<label asp-for="Data.SpWantAssertionsSigned" class="form-check-label"></label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="Data.SpValidateCertificates" type="checkbox" class="form-check-input">
<label asp-for="Data.SpValidateCertificates" class="form-check-label"></label>
</div>
</div>
</div>
<!-- SAML2 IDP -->
<div class="config-section">
<h2>@i18nService.T("SamlIdpConfig")</h2>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.IdpEntityId">@i18nService.T("EntityId")</label>
<input asp-for="Data.IdpEntityId" class="form-control">
<span asp-validation-for="Data.IdpEntityId" class="text-danger"></span>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.IdpBindingType"></label>
<select asp-for="Data.IdpBindingType" asp-items="Model.BindingTypes" class="form-control"></select>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.IdpSingleSignOnServiceUrl">@i18nService.T("SingleSignOnServiceUrl")</label>
<input asp-for="Data.IdpSingleSignOnServiceUrl" class="form-control">
<span asp-validation-for="Data.IdpSingleSignOnServiceUrl" class="text-danger"></span>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.IdpSingleLogoutServiceUrl">@i18nService.T("SingleLogoutServiceUrl")</label>
<input asp-for="Data.IdpSingleLogoutServiceUrl" class="form-control">
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.IdpArtifactResolutionServiceUrl"></label>
<input asp-for="Data.IdpArtifactResolutionServiceUrl" class="form-control">
<span asp-validation-for="Data.IdpArtifactResolutionServiceUrl" class="text-danger"></span>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.IdpX509PublicCert">@i18nService.T("X509PublicCert")</label>
<textarea asp-for="Data.IdpX509PublicCert" class="form-control"></textarea>
</div>
</div>
<div class="row">
<div class="col-6 form-group">
<label asp-for="Data.IdpOutboundSigningAlgorithm"></label>
<select asp-for="Data.IdpOutboundSigningAlgorithm" asp-items="Model.SigningAlgorithms"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="Data.IdpAllowUnsolicitedAuthnResponse" type="checkbox" class="form-check-input">
<label asp-for="Data.IdpAllowUnsolicitedAuthnResponse" class="form-check-label"></label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="Data.IdpDisableOutboundLogoutRequests" type="checkbox" class="form-check-input">
<label asp-for="Data.IdpDisableOutboundLogoutRequests" class="form-check-label"></label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="Data.IdpWantAuthnRequestsSigned" type="checkbox" class="form-check-input">
<label asp-for="Data.IdpWantAuthnRequestsSigned" class="form-check-label"></label>
</div>
</div>
</div>
</div>
</form>
<div class="d-flex">
<button type="submit" class="btn btn-primary" form="edit-form">@i18nService.T("Save")</button>
<a class="btn btn-outline-secondary ml-1" asp-action="index">@i18nService.T("Cancel")</a>
</div>

View File

@ -0,0 +1,4 @@
@using Microsoft.AspNetCore.Identity
@using Bit.Portal
@using Bit.Portal.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}

View File

@ -0,0 +1,23 @@
{
"globalSettings": {
"baseServiceUri": {
"vault": "https://vault.bitwarden.com",
"api": "https://api.bitwarden.com",
"identity": "https://identity.bitwarden.com",
"admin": "https://admin.bitwarden.com",
"notifications": "https://notifications.bitwarden.com",
"sso": "https://sso.bitwarden.com",
"portal": "http://portal.bitwarden.com",
"internalNotifications": "https://notifications.bitwarden.com",
"internalAdmin": "https://admin.bitwarden.com",
"internalIdentity": "https://identity.bitwarden.com",
"internalApi": "https://api.bitwarden.com",
"internalVault": "https://vault.bitwarden.com",
"internalSso": "https://sso.bitwarden.com",
"internalPortal": "https://portal.bitwarden.com"
},
"braintree": {
"production": true
}
}
}

View File

@ -0,0 +1,64 @@
{
"globalSettings": {
"selfHosted": false,
"siteName": "Bitwarden",
"projectName": "Business Portal",
"stripeApiKey": "SECRET",
"baseServiceUri": {
"vault": "https://localhost:8080",
"api": "http://localhost:4000",
"identity": "http://localhost:33656",
"admin": "http://localhost:62911",
"notifications": "http://localhost:61840",
"sso": "http://localhost:51822",
"portal": "http://localhost:52313",
"internalNotifications": "http://localhost:61840",
"internalAdmin": "http://localhost:62911",
"internalIdentity": "http://localhost:33656",
"internalApi": "http://localhost:4000",
"internalVault": "http://localhost:4001",
"internalSso": "http://localhost:51822",
"internalPortal": "http://localhost:52313"
},
"sqlServer": {
"connectionString": "SECRET"
},
"mail": {
"sendGridApiKey": "SECRET",
"amazonConfigSetName": "Email",
"replyToEmail": "no-reply@bitwarden.com"
},
"identityServer": {
"certificateThumbprint": "SECRET"
},
"dataProtection": {
"certificateThumbprint": "SECRET"
},
"storage": {
"connectionString": "SECRET"
},
"events": {
"connectionString": "SECRET"
},
"serviceBus": {
"connectionString": "SECRET",
"applicationCacheTopicName": "SECRET"
},
"documentDb": {
"uri": "SECRET",
"key": "SECRET"
},
"notificationHub": {
"connectionString": "SECRET",
"hubName": "SECRET"
},
"amazon": {
"accessKeyId": "SECRET",
"accessKeySecret": "SECRET",
"region": "SECRET"
}
},
"portalSettings": {
}
}

View File

@ -0,0 +1,18 @@
$curDir = pwd
$dir = Split-Path -Parent $MyInvocation.MyCommand.Path
echo "`n## Building Portal"
echo "`nBuilding app"
echo ".NET Core version $(dotnet --version)"
echo "Restore"
dotnet restore $dir\Portal.csproj
echo "Clean"
dotnet clean $dir\Portal.csproj -c "Release" -o $dir\obj\Azure\publish
echo "Node Build"
cd $dir
npm install
cd $curDir
gulp --gulpfile $dir\gulpfile.js build
echo "Publish"
dotnet publish $dir\Portal.csproj -c "Release" -o $dir\obj\Azure\publish

View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -e
CUR_DIR="$(pwd)"
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
echo -e "\n## Building Portal"
echo -e "\nBuilding app"
echo ".NET Core version $(dotnet --version)"
echo "Restore"
dotnet restore "$DIR/Portal.csproj"
echo "Clean"
dotnet clean "$DIR/Portal.csproj" -c "Release" -o "$DIR/obj/Docker/publish"
echo "Node Build"
cd "$DIR"
npm install
cd "$CUR_DIR"
gulp --gulpfile "$DIR/gulpfile.js" build
echo "Publish"
dotnet publish "$DIR/Portal.csproj" -c "Release" -o "$DIR/obj/Docker/publish"
echo -e "\nBuilding docker image"
docker --version
docker build -t bitwarden/portal "$DIR/."

View File

@ -0,0 +1,41 @@
#!/bin/bash
# Setup
GROUPNAME="bitwarden"
USERNAME="bitwarden"
LUID=${LOCAL_UID:-0}
LGID=${LOCAL_GID:-0}
# Step down from host root to well-known nobody/nogroup user
if [ $LUID -eq 0 ]
then
LUID=65534
fi
if [ $LGID -eq 0 ]
then
LGID=65534
fi
# Create user and group
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
mkhomedir_helper $USERNAME
# The rest...
chown -R $USERNAME:$GROUPNAME /app
mkdir -p /etc/bitwarden/core
mkdir -p /etc/bitwarden/logs
mkdir -p /etc/bitwarden/ca-certificates
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \
&& update-ca-certificates
exec gosu $USERNAME:$GROUPNAME dotnet /app/Portal.dll

View File

@ -0,0 +1,82 @@
/// <binding BeforeBuild='build' Clean='clean' ProjectOpened='build' />
const gulp = require('gulp');
const merge = require('merge-stream');
const googleWebFonts = require('gulp-google-webfonts');
const sass = require('gulp-sass');
const del = require('del');
const paths = {};
paths.webroot = './wwwroot/';
paths.npmDir = './node_modules/';
paths.sassDir = './Sass/';
paths.libDir = paths.webroot + 'lib/';
paths.cssDir = paths.webroot + 'css/';
paths.jsDir = paths.webroot + 'js/';
paths.sass = paths.sassDir + '**/*.scss';
paths.minCss = paths.cssDir + '**/*.min.css';
paths.js = paths.jsDir + '**/*.js';
paths.minJs = paths.jsDir + '**/*.min.js';
paths.libJs = paths.libDir + '**/*.js';
paths.libMinJs = paths.libDir + '**/*.min.js';
function clean() {
return del([paths.minJs, paths.cssDir, paths.libDir]);
}
function lib() {
const libs = [
{
src: paths.npmDir + 'bootstrap/dist/js/*',
dest: paths.libDir + 'bootstrap/js'
},
{
src: paths.npmDir + 'popper.js/dist/umd/*',
dest: paths.libDir + 'popper'
},
{
src: paths.npmDir + 'font-awesome/css/*',
dest: paths.libDir + 'font-awesome/css'
},
{
src: paths.npmDir + 'font-awesome/fonts/*',
dest: paths.libDir + 'font-awesome/fonts'
},
{
src: paths.npmDir + 'jquery/dist/jquery.slim*',
dest: paths.libDir + 'jquery'
},
];
const tasks = libs.map((lib) => {
return gulp.src(lib.src).pipe(gulp.dest(lib.dest));
});
return merge(tasks);
}
function runSass() {
return gulp.src(paths.sass)
.pipe(sass({ outputStyle: 'compressed' }).on('error', sass.logError))
.pipe(gulp.dest(paths.cssDir));
}
function sassWatch() {
gulp.watch(paths.sass, runSass);
}
function webfonts() {
return gulp.src('./webfonts.list')
.pipe(googleWebFonts({
fontsDir: 'webfonts',
cssFilename: 'webfonts.css'
}))
.pipe(gulp.dest(paths.cssDir));
}
exports.build = gulp.series(clean, gulp.parallel([lib, runSass, webfonts]));
exports['sass:watch'] = sassWatch;
exports.sass = runSass;
exports.lib = lib;
exports.webfonts = webfonts;
exports.clean = clean;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
{
"name": "bitwarden-enterprise-portal",
"version": "0.0.0",
"description": "Bitwarden Enterprise Portal",
"repository": "https://github.com/bitwarden/enterprise",
"license": "-",
"devDependencies": {
"bootstrap": "4.5.0",
"del": "5.1.0",
"font-awesome": "4.7.0",
"gulp": "4.0.2",
"gulp-google-webfonts": "2.0.0",
"gulp-sass": "4.0.1",
"jquery": "3.5.1",
"merge-stream": "1.0.1",
"popper.js": "1.16.1"
}
}

View File

@ -0,0 +1 @@
Open+Sans:300,300i,400,400i,600,600i,700,700i,800,800i&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,80 @@
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: url(webfonts/Open_Sans-italic-300.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: url(webfonts/Open_Sans-italic-400.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: url(webfonts/Open_Sans-italic-600.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: url(webfonts/Open_Sans-italic-700.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: url(webfonts/Open_Sans-italic-800.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: url(webfonts/Open_Sans-normal-300.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: url(webfonts/Open_Sans-normal-400.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: url(webfonts/Open_Sans-normal-600.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: url(webfonts/Open_Sans-normal-700.woff) format('woff');
unicode-range: U+0-10FFFF;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: url(webfonts/Open_Sans-normal-800.woff) format('woff');
unicode-range: U+0-10FFFF;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,4 @@
// Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,156 @@
// @flow
export type Position = 'top' | 'right' | 'bottom' | 'left';
export type Placement =
| 'auto-start'
| 'auto'
| 'auto-end'
| 'top-start'
| 'top'
| 'top-end'
| 'right-start'
| 'right'
| 'right-end'
| 'bottom-end'
| 'bottom'
| 'bottom-start'
| 'left-end'
| 'left'
| 'left-start';
export type Offset = {
top: number,
left: number,
width: number,
height: number,
position: Position,
};
export type Boundary = 'scrollParent' | 'viewport' | 'window';
export type Behavior = 'flip' | 'clockwise' | 'counterclockwise';
export type Data = {
instance: Popper,
placement: Placement,
originalPlacement: Placement,
flipped: boolean,
hide: boolean,
arrowElement: Element,
styles: CSSStyleDeclaration,
arrowStyles: CSSStyleDeclaration,
boundaries: Object,
offsets: {
popper: Offset,
reference: Offset,
arrow: {
top: number,
left: number,
},
},
};
export type ModifierFn = (data: Data, options: Object) => Data;
export type Padding = {
top?: number,
bottom?: number,
left?: number,
right?: number,
};
export type BaseModifier = {
order?: number,
enabled?: boolean,
fn?: ModifierFn,
};
export type Modifiers = {
shift?: BaseModifier,
offset?: BaseModifier & {
offset?: number | string,
},
preventOverflow?: BaseModifier & {
priority?: Position[],
padding?: number | Padding,
boundariesElement?: Boundary | Element,
escapeWithReference?: boolean,
},
keepTogether?: BaseModifier,
arrow?: BaseModifier & {
element?: string | Element | null,
},
flip?: BaseModifier & {
behavior?: Behavior | Position[],
padding?: number | Padding,
boundariesElement?: Boundary | Element,
flipVariations?: boolean,
flipVariationsByContent?: boolean,
},
inner?: BaseModifier,
hide?: BaseModifier,
applyStyle?: BaseModifier & {
onLoad?: Function,
gpuAcceleration?: boolean,
},
computeStyle?: BaseModifier & {
gpuAcceleration?: boolean,
x?: 'bottom' | 'top',
y?: 'left' | 'right',
},
[name: string]: (BaseModifier & { [string]: * }) | null,
};
export type Options = {
placement?: Placement,
positionFixed?: boolean,
eventsEnabled?: boolean,
modifiers?: Modifiers,
removeOnDestroy?: boolean,
onCreate?: (data: Data) => void,
onUpdate?: (data: Data) => void,
};
export type ReferenceObject = {
+clientHeight: number,
+clientWidth: number,
+referenceNode?: Node,
getBoundingClientRect():
| ClientRect
| {
width: number,
height: number,
top: number,
right: number,
bottom: number,
left: number,
},
};
export type Instance = {
destroy: () => void,
scheduleUpdate: () => void,
update: () => void,
enableEventListeners: () => void,
disableEventListeners: () => void,
};
declare class Popper {
static placements: Placement;
popper: Element;
reference: Element | ReferenceObject;
constructor(
reference: Element | ReferenceObject,
popper: Element,
options?: Options
): Instance;
}
declare export default typeof Popper;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,603 @@
using Bit.Core;
using Bit.Core.Enums;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Sso.Models;
using Bit.Sso.Utilities;
using IdentityModel;
using IdentityServer4;
using IdentityServer4.Extensions;
using IdentityServer4.Services;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Bit.Core.Models.Api;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
using System.Net;
namespace Bit.Sso.Controllers
{
public class AccountController : Controller
{
private readonly IAuthenticationSchemeProvider _schemeProvider;
private readonly IClientStore _clientStore;
private readonly IIdentityServerInteractionService _interaction;
private readonly ILogger<AccountController> _logger;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly ISsoUserRepository _ssoUserRepository;
private readonly IUserRepository _userRepository;
private readonly IUserService _userService;
private readonly II18nService _i18nService;
private readonly UserManager<User> _userManager;
public AccountController(
IAuthenticationSchemeProvider schemeProvider,
IClientStore clientStore,
IIdentityServerInteractionService interaction,
ILogger<AccountController> logger,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ISsoConfigRepository ssoConfigRepository,
ISsoUserRepository ssoUserRepository,
IUserRepository userRepository,
IUserService userService,
II18nService i18nService,
UserManager<User> userManager)
{
_schemeProvider = schemeProvider;
_clientStore = clientStore;
_interaction = interaction;
_logger = logger;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_userRepository = userRepository;
_ssoConfigRepository = ssoConfigRepository;
_ssoUserRepository = ssoUserRepository;
_userService = userService;
_i18nService = i18nService;
_userManager = userManager;
}
[HttpGet]
public async Task<IActionResult> PreValidate(string domainHint)
{
IActionResult invalidJson(string errorMessageKey, Exception ex = null)
{
Response.StatusCode = ex == null ? 400 : 500;
return Json(new ErrorResponseModel(_i18nService.T(errorMessageKey))
{
ExceptionMessage = ex?.Message,
ExceptionStackTrace = ex?.StackTrace,
InnerExceptionMessage = ex?.InnerException?.Message,
});
}
try
{
// Validate domain_hint provided
if (string.IsNullOrWhiteSpace(domainHint))
{
return invalidJson("NoOrganizationIdentifierProvidedError");
}
// Validate organization exists from domain_hint
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
if (organization == null)
{
return invalidJson("OrganizationNotFoundByIdentifierError");
}
if (!organization.UseSso)
{
return invalidJson("SsoNotAllowedForOrganizationError");
}
// Validate SsoConfig exists and is Enabled
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
if (ssoConfig == null)
{
return invalidJson("SsoConfigurationNotFoundForOrganizationError");
}
if (!ssoConfig.Enabled)
{
return invalidJson("SsoNotEnabledForOrganizationError");
}
// Validate Authentication Scheme exists and is loaded (cache)
var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString());
if (scheme == null || !(scheme is IDynamicAuthenticationScheme dynamicScheme))
{
return invalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError");
}
// Run scheme validation
try
{
await dynamicScheme.Validate();
}
catch (Exception ex)
{
var translatedException = _i18nService.GetLocalizedHtmlString(ex.Message);
var errorKey = "InvalidSchemeConfigurationError";
if (!translatedException.ResourceNotFound)
{
errorKey = ex.Message;
}
return invalidJson(errorKey, translatedException.ResourceNotFound ? ex : null);
}
}
catch (Exception ex)
{
return invalidJson("PreValidationError", ex);
}
// Everything is good!
return new EmptyResult();
}
[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
{
scheme = context.Parameters["domain_hint"],
returnUrl,
state = context.Parameters["state"],
userIdentifier = context.Parameters["session_state"]
});
}
else
{
throw new Exception("No domain_hint provided.");
}
}
[HttpGet]
public IActionResult ExternalChallenge(string scheme, string returnUrl, string state, string userIdentifier)
{
if (string.IsNullOrEmpty(returnUrl))
{
returnUrl = "~/";
}
if (!Url.IsLocalUrl(returnUrl) && !_interaction.IsValidReturnUrl(returnUrl))
{
throw new Exception("invalid return URL");
}
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(ExternalCallback)),
Items =
{
// scheme will get serialized into `State` and returned back
{ "scheme", scheme },
{ "return_url", returnUrl },
{ "state", state },
{ "user_identifier", userIdentifier },
}
};
return Challenge(props, scheme);
}
[HttpGet]
public async Task<IActionResult> ExternalCallback()
{
// Read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(
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);
// Lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);
if (user == null)
{
// This might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier") ?
result.Properties.Items["user_identifier"] : null;
user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier);
}
if (user != null)
{
// This allows us to collect any additional claims or properties
// for the specific protocols 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)
};
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// Issue authentication cookie for user
await HttpContext.SignInAsync(new IdentityServerUser(user.Id.ToString())
{
DisplayName = user.Email,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims.ToArray()
}, localSignInProps);
}
// Delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// Retrieve return URL
var returnUrl = result.Properties.Items["return_url"] ?? "~/";
// Check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context != null)
{
if (IsNativeClient(context))
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
HttpContext.Response.StatusCode = 200;
HttpContext.Response.Headers["Location"] = string.Empty;
return View("Redirect", new RedirectViewModel { RedirectUrl = returnUrl });
}
}
return Redirect(returnUrl);
}
[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
// Build a model so the logged out page knows what to display
var (updatedLogoutId, redirectUri, externalAuthenticationScheme) = await GetLoggedOutDataAsync(logoutId);
if (User?.Identity.IsAuthenticated == true)
{
// Delete local authentication cookie
await HttpContext.SignOutAsync();
}
// HACK: Temporary workaroud for the time being that doesn't try to sign out of OneLogin schemes,
// which doesnt support SLO
if (externalAuthenticationScheme != null && !externalAuthenticationScheme.Contains("onelogin"))
{
// Build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
var url = Url.Action("Logout", new { logoutId = updatedLogoutId });
// This triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, externalAuthenticationScheme);
}
if (redirectUri != null)
{
return View("Redirect", new RedirectViewModel { RedirectUrl = redirectUri });
}
else
{
return Redirect("~/");
}
}
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims)>
FindUserFromExternalProviderAsync(AuthenticateResult result)
{
var externalUser = result.Principal;
// Ensure the NameIdentifier used is not a transient name ID, if so, we need a different attribute
// for the user identifier.
static bool nameIdIsNotTransient(Claim c) => c.Type == ClaimTypes.NameIdentifier
&& (c.Properties == null
|| !c.Properties.ContainsKey(SamlPropertyKeys.ClaimFormat)
|| c.Properties[SamlPropertyKeys.ClaimFormat] != SamlNameIdFormats.Transient);
// 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(nameIdIsNotTransient) ??
// Some SAML providers may use the `uid` attribute for this
// where a transient NameID has been sent in the subject
externalUser.FindFirst("uid") ??
externalUser.FindFirst("upn") ??
externalUser.FindFirst("eppn") ??
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;
// find external user
var orgId = new Guid(provider);
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);
if (ssoConfig == null || !ssoConfig.Enabled)
{
throw new Exception("Organization not found or SSO configuration not enabled");
}
var user = await _userRepository.GetBySsoUserAsync(providerUserId, orgId);
return (user, provider, providerUserId, claims);
}
private async Task<User> AutoProvisionUserAsync(string provider, string providerUserId,
IEnumerable<Claim> claims, string userIdentifier)
{
var name = GetName(claims);
var email = GetEmailAddress(claims);
Guid? orgId = null;
if (Guid.TryParse(provider, out var oId))
{
orgId = oId;
}
else
{
// TODO: support non-org (server-wide) SSO in the future?
throw new Exception($"SSO provider, '{provider}' is not an organization id");
}
User existingUser = null;
if (string.IsNullOrWhiteSpace(userIdentifier))
{
if (string.IsNullOrWhiteSpace(email))
{
throw new Exception("Cannot find email claim");
}
existingUser = await _userRepository.GetByEmailAsync(email);
}
else
{
var bytes = System.Convert.FromBase64String(userIdentifier);
userIdentifier = System.Text.Encoding.UTF8.GetString(bytes);
var split = userIdentifier.Split(",");
if (split.Length < 2)
{
throw new Exception("Invalid user identifier.");
}
var userId = split[0];
var token = split[1];
var tokenOptions = new TokenOptions();
var claimedUser = await _userService.GetUserByIdAsync(userId);
if (claimedUser != null)
{
var tokenIsValid = await _userManager.VerifyUserTokenAsync(
claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token);
if (tokenIsValid)
{
existingUser = claimedUser;
}
else
{
throw new Exception("Supplied userId and token did not match.");
}
}
}
OrganizationUser orgUser = null;
if (orgId.HasValue)
{
var organization = await _organizationRepository.GetByIdAsync(orgId.Value);
if (organization == null)
{
throw new Exception($"Could not find organization for '{orgId}'");
}
if (existingUser != null)
{
var orgUsers = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id);
orgUser = orgUsers.SingleOrDefault(u => u.OrganizationId == orgId.Value &&
u.Status != OrganizationUserStatusType.Invited);
}
if (orgUser == null)
{
if (organization.Seats.HasValue)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(orgId.Value);
var availableSeats = organization.Seats.Value - userCount;
if (availableSeats < 1)
{
// No seats are available
throw new Exception($"No seats available for organization, '{organization.Name}'");
}
}
// Make sure user is not already invited to this org
var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
orgId.Value, email, false);
if (existingOrgUserCount > 0)
{
throw new Exception($"User, '{email}', has already been invited to this organization, '{organization.Name}'");
}
}
}
User user = null;
if (orgUser == null)
{
if (existingUser != null)
{
// TODO: send an email inviting this user to link SSO to their account?
throw new Exception("User already exists, please link account to SSO after logging in");
}
// Create user record
user = new User
{
Name = name,
Email = email
};
await _userService.RegisterUserAsync(user);
if (orgId.HasValue)
{
// Create organization user record
orgUser = new OrganizationUser
{
OrganizationId = orgId.Value,
UserId = user.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Accepted
};
await _organizationUserRepository.CreateAsync(orgUser);
}
}
else
{
// Since the user is already a member of this organization, let's link their existing user account
user = existingUser;
}
// Create sso user record
var ssoUser = new SsoUser
{
ExternalId = providerUserId,
UserId = user.Id,
OrganizationId = orgId
};
await _ssoUserRepository.CreateAsync(ssoUser);
return user;
}
private string GetEmailAddress(IEnumerable<Claim> claims)
{
var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value) && c.Value.Contains("@"));
var email = filteredClaims.GetFirstMatch(JwtClaimTypes.Email, ClaimTypes.Email,
SamlClaimTypes.Email, "mail", "emailaddress");
if (!string.IsNullOrWhiteSpace(email))
{
return email;
}
var username = filteredClaims.GetFirstMatch(JwtClaimTypes.PreferredUserName,
SamlClaimTypes.UserId, "uid");
if (!string.IsNullOrWhiteSpace(username))
{
return username;
}
return null;
}
private string GetName(IEnumerable<Claim> claims)
{
var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value));
var name = filteredClaims.GetFirstMatch(JwtClaimTypes.Name, ClaimTypes.Name,
SamlClaimTypes.DisplayName, SamlClaimTypes.CommonName, "displayname", "cn");
if (!string.IsNullOrWhiteSpace(name))
{
return name;
}
var givenName = filteredClaims.GetFirstMatch(SamlClaimTypes.GivenName, "givenname", "firstname",
"fn", "fname", "nickname");
var surname = filteredClaims.GetFirstMatch(SamlClaimTypes.Surname, "sn", "surname", "lastname");
var nameParts = new[] { givenName, surname }.Where(p => !string.IsNullOrWhiteSpace(p));
if (nameParts.Any())
{
return string.Join(' ', nameParts);
}
return null;
}
private void ProcessLoginCallback(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 idToken, we'll keep it for signout
var idToken = externalResult.Properties.GetTokenValue("id_token");
if (idToken != null)
{
localSignInProps.StoreTokens(
new[] { new AuthenticationToken { Name = "id_token", Value = idToken } });
}
}
private async Task<string> GetProviderAsync(string returnUrl)
{
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
{
return context.IdP;
}
var schemes = await _schemeProvider.GetAllSchemesAsync();
var providers = schemes.Select(x => x.Name).ToList();
return providers.FirstOrDefault();
}
private async Task<(string, string, string)> GetLoggedOutDataAsync(string logoutId)
{
// Get context information (client name, post logout redirect URI and iframe for federated signout)
var logout = await _interaction.GetLogoutContextAsync(logoutId);
string externalAuthenticationScheme = null;
if (User?.Identity.IsAuthenticated == true)
{
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)
{
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
if (providerSupportsSignout)
{
if (logoutId == null)
{
// If there's no current logout context, we need to create one
// this captures necessary info from the current logged in user
// before we signout and redirect away to the external IdP for signout
logoutId = await _interaction.CreateLogoutContextAsync();
}
externalAuthenticationScheme = idp;
}
}
}
return (logoutId, logout?.PostLogoutRedirectUri, externalAuthenticationScheme);
}
public bool IsNativeClient(IdentityServer4.Models.AuthorizationRequest context)
{
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
}
}
}

View File

@ -0,0 +1,43 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using IdentityServer4.Services;
using System.Threading.Tasks;
using Bit.Sso.Models;
namespace Bit.Sso.Controllers
{
public class HomeController : Controller
{
private readonly IIdentityServerInteractionService _interaction;
public HomeController(IIdentityServerInteractionService interaction)
{
_interaction = interaction;
}
[HttpGet("~/alive")]
[HttpGet("~/now")]
[AllowAnonymous]
public DateTime GetAlive()
{
return DateTime.UtcNow;
}
[HttpGet("~/Error")]
[HttpGet("~/Home/Error")]
public async Task<IActionResult> Error(string errorId)
{
var vm = new ErrorViewModel();
// retrieve error details from identityserver
var message = await _interaction.GetErrorContextAsync(errorId);
if (message != null)
{
vm.Error = message;
}
return View("Error", vm);
}
}
}

View File

@ -0,0 +1,20 @@
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
LABEL com.bitwarden.product="bitwarden"
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
ENV ASPNETCORE_URLS http://+:5000
WORKDIR /app
EXPOSE 5000
COPY obj/Docker/publish .
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh
HEALTHCHECK CMD curl -f http://localhost:5000/alive || exit 1
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -0,0 +1,15 @@
using System;
using IdentityServer4.Models;
namespace Bit.Sso.Models
{
public class ErrorViewModel
{
public ErrorMessage Error { get; set; }
public string Message => Error?.Error;
public string Description => Error?.ErrorDescription;
public string RequestId => Error?.RequestId;
public string RedirectUri => Error?.RedirectUri;
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Sso.Models
{
public class RedirectViewModel
{
public string RedirectUrl { get; set; }
}
}

View File

@ -0,0 +1,9 @@
using System.Security.Cryptography.X509Certificates;
namespace Bit.Sso.Models
{
public class SamlEnvironment
{
public X509Certificate2 SpSigningCertificate { get; set; }
}
}

View File

@ -0,0 +1,35 @@
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Events;
namespace Bit.Sso
{
public class Program
{
public static void Main(string[] args)
{
Host
.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureLogging((hostingContext, logging) =>
logging.AddSerilog(hostingContext, e =>
{
var context = e.Properties["SourceContext"].ToString();
if (e.Properties.ContainsKey("RequestPath") &&
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
{
return false;
}
return e.Level >= LogEventLevel.Error;
}));
})
.Build()
.Run();
}
}
}

View File

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:51822",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Sso": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:51822",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,33 @@
$primary: #175DDC;
$primary-accent: #1252A3;
$success: #00a65a;
$info: #555555;
$warning: #bf7e16;
$danger: #dd4b39;
$theme-colors: ( "primary-accent": $primary-accent );
$font-family-sans-serif: 'Open Sans','Helvetica Neue',Helvetica,Arial,sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
$h1-font-size: 2rem;
$h2-font-size: 1.3rem;
$h3-font-size: 1rem;
$h4-font-size: 1rem;
$h5-font-size: 1rem;
$h6-font-size: 1rem;
@import "../node_modules/bootstrap/scss/bootstrap.scss";
h1 {
border-bottom: 1px solid $border-color;
margin-bottom: 20px;
small {
color: $text-muted;
font-size: $h1-font-size * .5;
}
}
h2 {
text-transform: uppercase;
font-weight: bold;
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>Bit.Sso</RootNamespace>
<UserSecretsId>bitwarden-Sso</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.7.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\Core\Core.csproj" />
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More