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:
parent
61dff9c758
commit
84c85a90e8
@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.29102.190
|
||||
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
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "util", "util", "{DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}"
|
||||
EndProject
|
||||
@ -53,6 +53,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrator", "util\Migrator\M
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Api.Test", "test\Api.Test\Api.Test.csproj", "{860DE301-0B3E-4717-9C21-A9B4C3C2B121}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -141,6 +155,8 @@ Global
|
||||
{79BB453F-D0D8-4DDF-9809-A405C56692BD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
|
||||
{54DED792-A022-417E-9804-21FCC9C7C610} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
{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
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
63
bitwarden_license/src/Portal/Controllers/AuthController.cs
Normal file
63
bitwarden_license/src/Portal/Controllers/AuthController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
70
bitwarden_license/src/Portal/Controllers/HomeController.cs
Normal file
70
bitwarden_license/src/Portal/Controllers/HomeController.cs
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
123
bitwarden_license/src/Portal/Controllers/PoliciesController.cs
Normal file
123
bitwarden_license/src/Portal/Controllers/PoliciesController.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
bitwarden_license/src/Portal/Controllers/SsoController.cs
Normal file
90
bitwarden_license/src/Portal/Controllers/SsoController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
20
bitwarden_license/src/Portal/Dockerfile
Normal file
20
bitwarden_license/src/Portal/Dockerfile
Normal 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"]
|
111
bitwarden_license/src/Portal/EnterprisePortalCurrentContext.cs
Normal file
111
bitwarden_license/src/Portal/EnterprisePortalCurrentContext.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
bitwarden_license/src/Portal/Models/ErrorViewModel.cs
Normal file
11
bitwarden_license/src/Portal/Models/ErrorViewModel.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
30
bitwarden_license/src/Portal/Models/PoliciesModel.cs
Normal file
30
bitwarden_license/src/Portal/Models/PoliciesModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
110
bitwarden_license/src/Portal/Models/PolicyEditModel.cs
Normal file
110
bitwarden_license/src/Portal/Models/PolicyEditModel.cs
Normal 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") },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
49
bitwarden_license/src/Portal/Models/PolicyModel.cs
Normal file
49
bitwarden_license/src/Portal/Models/PolicyModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
175
bitwarden_license/src/Portal/Models/SsoConfigDataViewModel.cs
Normal file
175
bitwarden_license/src/Portal/Models/SsoConfigDataViewModel.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
108
bitwarden_license/src/Portal/Models/SsoConfigEditViewModel.cs
Normal file
108
bitwarden_license/src/Portal/Models/SsoConfigEditViewModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
14
bitwarden_license/src/Portal/Portal.csproj
Normal file
14
bitwarden_license/src/Portal/Portal.csproj
Normal 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>
|
8
bitwarden_license/src/Portal/PortalSettings.cs
Normal file
8
bitwarden_license/src/Portal/PortalSettings.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.Portal
|
||||
{
|
||||
public class PortalSettings
|
||||
{
|
||||
}
|
||||
}
|
35
bitwarden_license/src/Portal/Program.cs
Normal file
35
bitwarden_license/src/Portal/Program.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
5
bitwarden_license/src/Portal/Properties/AssemblyInfo.cs
Normal file
5
bitwarden_license/src/Portal/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,5 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
[assembly: ResourceLocation("Resources")]
|
||||
[assembly: RootNamespace("Bit.Portal")]
|
27
bitwarden_license/src/Portal/Properties/launchSettings.json
Normal file
27
bitwarden_license/src/Portal/Properties/launchSettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
226
bitwarden_license/src/Portal/Sass/site.scss
Normal file
226
bitwarden_license/src/Portal/Sass/site.scss
Normal 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;
|
||||
}
|
110
bitwarden_license/src/Portal/Startup.cs
Normal file
110
bitwarden_license/src/Portal/Startup.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
@inject Bit.Core.Services.II18nService i18nService
|
||||
@{
|
||||
ViewData["Title"] = i18nService.T("AccessDenied");
|
||||
}
|
||||
|
||||
<p>
|
||||
@i18nService.T("AccessDeniedError")
|
||||
</p>
|
8
bitwarden_license/src/Portal/Views/Auth/LoggedOut.cshtml
Normal file
8
bitwarden_license/src/Portal/Views/Auth/LoggedOut.cshtml
Normal file
@ -0,0 +1,8 @@
|
||||
@inject Bit.Core.Services.II18nService i18nService
|
||||
@{
|
||||
ViewData["Title"] = i18nService.T("LoggedOut");
|
||||
}
|
||||
|
||||
<p>
|
||||
@i18nService.T("LoggedOutMessage")
|
||||
</p>
|
8
bitwarden_license/src/Portal/Views/Home/Index.cshtml
Normal file
8
bitwarden_license/src/Portal/Views/Home/Index.cshtml
Normal file
@ -0,0 +1,8 @@
|
||||
@{
|
||||
ViewData["Title"] = "Home Page";
|
||||
}
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Welcome</h1>
|
||||
</div>
|
||||
<p>Hello world...</p>
|
141
bitwarden_license/src/Portal/Views/Policies/Edit.cshtml
Normal file
141
bitwarden_license/src/Portal/Views/Policies/Edit.cshtml
Normal 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>
|
29
bitwarden_license/src/Portal/Views/Policies/Index.cshtml
Normal file
29
bitwarden_license/src/Portal/Views/Policies/Index.cshtml
Normal 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>
|
@ -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>
|
25
bitwarden_license/src/Portal/Views/Shared/Error.cshtml
Normal file
25
bitwarden_license/src/Portal/Views/Shared/Error.cshtml
Normal 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>
|
121
bitwarden_license/src/Portal/Views/Shared/_Layout.cshtml
Normal file
121
bitwarden_license/src/Portal/Views/Shared/_Layout.cshtml
Normal 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">
|
||||
© @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>
|
226
bitwarden_license/src/Portal/Views/Sso/Index.cshtml
Normal file
226
bitwarden_license/src/Portal/Views/Sso/Index.cshtml
Normal 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>
|
4
bitwarden_license/src/Portal/Views/_ViewImports.cshtml
Normal file
4
bitwarden_license/src/Portal/Views/_ViewImports.cshtml
Normal file
@ -0,0 +1,4 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Bit.Portal
|
||||
@using Bit.Portal.Models
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
3
bitwarden_license/src/Portal/Views/_ViewStart.cshtml
Normal file
3
bitwarden_license/src/Portal/Views/_ViewStart.cshtml
Normal file
@ -0,0 +1,3 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
23
bitwarden_license/src/Portal/appsettings.Production.json
Normal file
23
bitwarden_license/src/Portal/appsettings.Production.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
64
bitwarden_license/src/Portal/appsettings.json
Normal file
64
bitwarden_license/src/Portal/appsettings.json
Normal 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": {
|
||||
|
||||
}
|
||||
}
|
18
bitwarden_license/src/Portal/build.ps1
Normal file
18
bitwarden_license/src/Portal/build.ps1
Normal 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
|
25
bitwarden_license/src/Portal/build.sh
Normal file
25
bitwarden_license/src/Portal/build.sh
Normal 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/."
|
41
bitwarden_license/src/Portal/entrypoint.sh
Normal file
41
bitwarden_license/src/Portal/entrypoint.sh
Normal 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
|
82
bitwarden_license/src/Portal/gulpfile.js
Normal file
82
bitwarden_license/src/Portal/gulpfile.js
Normal 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;
|
4992
bitwarden_license/src/Portal/package-lock.json
generated
Normal file
4992
bitwarden_license/src/Portal/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
bitwarden_license/src/Portal/package.json
Normal file
18
bitwarden_license/src/Portal/package.json
Normal 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"
|
||||
}
|
||||
}
|
1
bitwarden_license/src/Portal/webfonts.list
Normal file
1
bitwarden_license/src/Portal/webfonts.list
Normal 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
|
6
bitwarden_license/src/Portal/wwwroot/css/site.css
Normal file
6
bitwarden_license/src/Portal/wwwroot/css/site.css
Normal file
File diff suppressed because one or more lines are too long
80
bitwarden_license/src/Portal/wwwroot/css/webfonts.css
Normal file
80
bitwarden_license/src/Portal/wwwroot/css/webfonts.css
Normal 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
bitwarden_license/src/Portal/wwwroot/favicon.ico
Normal file
BIN
bitwarden_license/src/Portal/wwwroot/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
4
bitwarden_license/src/Portal/wwwroot/js/site.js
Normal file
4
bitwarden_license/src/Portal/wwwroot/js/site.js
Normal 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.
|
7033
bitwarden_license/src/Portal/wwwroot/lib/bootstrap/js/bootstrap.bundle.js
vendored
Normal file
7033
bitwarden_license/src/Portal/wwwroot/lib/bootstrap/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
bitwarden_license/src/Portal/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js
vendored
Normal file
7
bitwarden_license/src/Portal/wwwroot/lib/bootstrap/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4420
bitwarden_license/src/Portal/wwwroot/lib/bootstrap/js/bootstrap.js
vendored
Normal file
4420
bitwarden_license/src/Portal/wwwroot/lib/bootstrap/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
bitwarden_license/src/Portal/wwwroot/lib/bootstrap/js/bootstrap.min.js
vendored
Normal file
7
bitwarden_license/src/Portal/wwwroot/lib/bootstrap/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2337
bitwarden_license/src/Portal/wwwroot/lib/font-awesome/css/font-awesome.css
vendored
Normal file
2337
bitwarden_license/src/Portal/wwwroot/lib/font-awesome/css/font-awesome.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
4
bitwarden_license/src/Portal/wwwroot/lib/font-awesome/css/font-awesome.min.css
vendored
Normal file
4
bitwarden_license/src/Portal/wwwroot/lib/font-awesome/css/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
8777
bitwarden_license/src/Portal/wwwroot/lib/jquery/jquery.slim.js
Normal file
8777
bitwarden_license/src/Portal/wwwroot/lib/jquery/jquery.slim.js
Normal file
File diff suppressed because it is too large
Load Diff
2
bitwarden_license/src/Portal/wwwroot/lib/jquery/jquery.slim.min.js
vendored
Normal file
2
bitwarden_license/src/Portal/wwwroot/lib/jquery/jquery.slim.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1151
bitwarden_license/src/Portal/wwwroot/lib/popper/popper-utils.js
Normal file
1151
bitwarden_license/src/Portal/wwwroot/lib/popper/popper-utils.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
5
bitwarden_license/src/Portal/wwwroot/lib/popper/popper-utils.min.js
vendored
Normal file
5
bitwarden_license/src/Portal/wwwroot/lib/popper/popper-utils.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2624
bitwarden_license/src/Portal/wwwroot/lib/popper/popper.js
Normal file
2624
bitwarden_license/src/Portal/wwwroot/lib/popper/popper.js
Normal file
File diff suppressed because it is too large
Load Diff
156
bitwarden_license/src/Portal/wwwroot/lib/popper/popper.js.flow
Normal file
156
bitwarden_license/src/Portal/wwwroot/lib/popper/popper.js.flow
Normal 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
5
bitwarden_license/src/Portal/wwwroot/lib/popper/popper.min.js
vendored
Normal file
5
bitwarden_license/src/Portal/wwwroot/lib/popper/popper.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
603
bitwarden_license/src/Sso/Controllers/AccountController.cs
Normal file
603
bitwarden_license/src/Sso/Controllers/AccountController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
43
bitwarden_license/src/Sso/Controllers/HomeController.cs
Normal file
43
bitwarden_license/src/Sso/Controllers/HomeController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
20
bitwarden_license/src/Sso/Dockerfile
Normal file
20
bitwarden_license/src/Sso/Dockerfile
Normal 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"]
|
15
bitwarden_license/src/Sso/Models/ErrorViewModel.cs
Normal file
15
bitwarden_license/src/Sso/Models/ErrorViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
7
bitwarden_license/src/Sso/Models/RedirectViewModel.cs
Normal file
7
bitwarden_license/src/Sso/Models/RedirectViewModel.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Sso.Models
|
||||
{
|
||||
public class RedirectViewModel
|
||||
{
|
||||
public string RedirectUrl { get; set; }
|
||||
}
|
||||
}
|
9
bitwarden_license/src/Sso/Models/SamlEnvironment.cs
Normal file
9
bitwarden_license/src/Sso/Models/SamlEnvironment.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Bit.Sso.Models
|
||||
{
|
||||
public class SamlEnvironment
|
||||
{
|
||||
public X509Certificate2 SpSigningCertificate { get; set; }
|
||||
}
|
||||
}
|
35
bitwarden_license/src/Sso/Program.cs
Normal file
35
bitwarden_license/src/Sso/Program.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
27
bitwarden_license/src/Sso/Properties/launchSettings.json
Normal file
27
bitwarden_license/src/Sso/Properties/launchSettings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
bitwarden_license/src/Sso/Sass/site.scss
Normal file
33
bitwarden_license/src/Sso/Sass/site.scss
Normal 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;
|
||||
}
|
16
bitwarden_license/src/Sso/Sso.csproj
Normal file
16
bitwarden_license/src/Sso/Sso.csproj
Normal 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
Loading…
Reference in New Issue
Block a user