mirror of
https://github.com/bitwarden/server.git
synced 2024-11-26 12:55:17 +01:00
passwordless sign in for admin
This commit is contained in:
parent
1be7701da0
commit
d35d8185ed
@ -27,4 +27,10 @@
|
||||
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="appsettings.json">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
7
src/Admin/AdminSettings.cs
Normal file
7
src/Admin/AdminSettings.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Admin
|
||||
{
|
||||
public class AdminSettings
|
||||
{
|
||||
public virtual string Admins { get; set; }
|
||||
}
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Bit.Admin.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Admin.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
[Authorize]
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
|
48
src/Admin/Controllers/LoginController.cs
Normal file
48
src/Admin/Controllers/LoginController.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Core.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Admin.Controllers
|
||||
{
|
||||
public class LoginController : Controller
|
||||
{
|
||||
private readonly PasswordlessSignInManager<IdentityUser> _signInManager;
|
||||
|
||||
public LoginController(
|
||||
PasswordlessSignInManager<IdentityUser> signInManager)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Index(LoginModel model)
|
||||
{
|
||||
if(ModelState.IsValid)
|
||||
{
|
||||
await _signInManager.PasswordlessSignInAsync(model.Email);
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Confirm(string email, string token)
|
||||
{
|
||||
var result = await _signInManager.PasswordlessSignInAsync(email, token, false);
|
||||
if(!result.Succeeded)
|
||||
{
|
||||
return View("Error");
|
||||
}
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
}
|
||||
}
|
11
src/Admin/Models/LoginModel.cs
Normal file
11
src/Admin/Models/LoginModel.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Admin.Models
|
||||
{
|
||||
public class LoginModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; }
|
||||
}
|
||||
}
|
@ -1,9 +1,17 @@
|
||||
using System;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Events;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Admin
|
||||
{
|
||||
@ -18,22 +26,52 @@ namespace Bit.Admin
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddMvc();
|
||||
// Options
|
||||
services.AddOptions();
|
||||
|
||||
// Settings
|
||||
var globalSettings = services.AddGlobalSettingsServices(Configuration);
|
||||
services.Configure<AdminSettings>(Configuration.GetSection("AdminSettings"));
|
||||
|
||||
// Stripe Billing
|
||||
StripeConfiguration.SetApiKey(globalSettings.StripeApiKey);
|
||||
|
||||
// Repositories
|
||||
services.AddSqlServerRepositories(globalSettings);
|
||||
|
||||
// Context
|
||||
services.AddScoped<CurrentContext>();
|
||||
|
||||
// Identity
|
||||
services.AddPasswordlessIdentityServices<ReadOnlyEnvIdentityUserStore>(globalSettings);
|
||||
|
||||
// Services
|
||||
services.AddBaseServices();
|
||||
services.AddDefaultServices(globalSettings);
|
||||
|
||||
// Mvc
|
||||
services.AddMvc(config =>
|
||||
{
|
||||
config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());
|
||||
});
|
||||
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IHostingEnvironment env,
|
||||
IApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
loggerFactory.AddSerilog(env, appLifetime, globalSettings, (e) => e.Level >= LogEventLevel.Error);
|
||||
|
||||
if(env.IsDevelopment())
|
||||
{
|
||||
app.UseBrowserLink();
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/home/error");
|
||||
}
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseStaticFiles();
|
||||
app.UseMvcWithDefaultRoute();
|
||||
}
|
||||
|
21
src/Admin/Views/Login/Index.cshtml
Normal file
21
src/Admin/Views/Login/Index.cshtml
Normal file
@ -0,0 +1,21 @@
|
||||
@model LoginModel
|
||||
@{
|
||||
ViewData["Title"] = "Login";
|
||||
}
|
||||
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col-4">
|
||||
<p>Please enter your email address below to log in.</p>
|
||||
<form asp-action="" method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Email" class="sr-only">Email Address</label>
|
||||
<input asp-for="Email" type="email" class="form-control" placeholder="ex. john@example.com"
|
||||
required autofocus>
|
||||
<span asp-validation-for="Email" class="invalid-feedback"></span>
|
||||
<small class="form-text text-muted">We'll email you a secure login link.</small>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" type="submit">Continue</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -1,10 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
}
|
||||
}
|
13
src/Admin/appsettings.Production.json
Normal file
13
src/Admin/appsettings.Production.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"globalSettings": {
|
||||
"baseServiceUri": {
|
||||
"vault": "https://vault.bitwarden.com",
|
||||
"api": "https://api.bitwarden.com",
|
||||
"identity": "https://identity.bitwarden.com",
|
||||
"internalIdentity": "https://identity.bitwarden.com"
|
||||
},
|
||||
"braintree": {
|
||||
"production": true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,50 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Warning"
|
||||
"globalSettings": {
|
||||
"selfHosted": false,
|
||||
"siteName": "Bitwarden",
|
||||
"projectName": "Admin",
|
||||
"stripeApiKey": "SECRET",
|
||||
"baseServiceUri": {
|
||||
"vault": "http://localhost:4001",
|
||||
"api": "http://localhost:4000",
|
||||
"identity": "http://localhost:33656",
|
||||
"internalIdentity": "http://localhost:33656"
|
||||
},
|
||||
"sqlServer": {
|
||||
"connectionString": "SECRET"
|
||||
},
|
||||
"mail": {
|
||||
"sendGridApiKey": "SECRET",
|
||||
"replyToEmail": "hello@bitwarden.com"
|
||||
},
|
||||
"identityServer": {
|
||||
"certificateThumbprint": "SECRET"
|
||||
},
|
||||
"dataProtection": {
|
||||
"certificateThumbprint": "SECRET"
|
||||
},
|
||||
"storage": {
|
||||
"connectionString": "SECRET"
|
||||
},
|
||||
"events": {
|
||||
"connectionString": "SECRET"
|
||||
},
|
||||
"documentDb": {
|
||||
"uri": "SECRET",
|
||||
"key": "SECRET"
|
||||
},
|
||||
"notificationHub": {
|
||||
"connectionString": "SECRET",
|
||||
"hubName": "SECRET"
|
||||
}
|
||||
},
|
||||
"adminSettings": {
|
||||
"admins": ""
|
||||
},
|
||||
"braintree": {
|
||||
"production": false,
|
||||
"merchantId": "SECRET",
|
||||
"publicKey": "SECRET",
|
||||
"privateKey": "SECRET"
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,7 @@ using Bit.Core.Utilities;
|
||||
using Serilog.Events;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Bit.Billing.Utilities;
|
||||
using Bit.Core.Identity;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Bit.Billing
|
||||
@ -45,28 +43,7 @@ namespace Bit.Billing
|
||||
services.AddScoped<CurrentContext>();
|
||||
|
||||
// Identity
|
||||
services.AddTransient<ILookupNormalizer, LowerInvariantLookupNormalizer>();
|
||||
services.AddIdentity<IdentityUser, Core.Models.Table.Role>()
|
||||
.AddUserStore<ReadOnlyIdentityUserStore>()
|
||||
.AddRoleStore<RoleStore>()
|
||||
.AddDefaultTokenProviders();
|
||||
services.TryAddScoped<PasswordlessSignInManager<IdentityUser>, PasswordlessSignInManager<IdentityUser>>();
|
||||
|
||||
services.Configure<DataProtectionTokenProviderOptions>(options =>
|
||||
{
|
||||
options.TokenLifespan = TimeSpan.FromMinutes(15);
|
||||
});
|
||||
services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/login";
|
||||
options.LogoutPath = "/";
|
||||
options.AccessDeniedPath = "/login";
|
||||
options.Cookie.Name = "BitwardenBilling";
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
|
||||
options.ReturnUrlParameter = "returnUrl";
|
||||
options.SlidingExpiration = true;
|
||||
});
|
||||
services.AddPasswordlessIdentityServices<ReadOnlyDatabaseIdentityUserStore>(globalSettings);
|
||||
|
||||
// Services
|
||||
services.AddBaseServices();
|
||||
@ -77,7 +54,7 @@ namespace Bit.Billing
|
||||
// Mvc
|
||||
services.AddMvc(config =>
|
||||
{
|
||||
config.Filters.Add(new ExceptionHandlerFilterAttribute());
|
||||
config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());
|
||||
});
|
||||
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
||||
}
|
||||
|
37
src/Core/Identity/ReadOnlyDatabaseIdentityUserStore.cs
Normal file
37
src/Core/Identity/ReadOnlyDatabaseIdentityUserStore.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.Identity
|
||||
{
|
||||
public class ReadOnlyDatabaseIdentityUserStore : ReadOnlyIdentityUserStore
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public ReadOnlyDatabaseIdentityUserStore(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public override async Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var user = await _userRepository.GetByEmailAsync(normalizedEmail);
|
||||
return user?.ToIdentityUser();
|
||||
}
|
||||
|
||||
public override async Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
if(!Guid.TryParse(userId, out var userIdGuid))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(userIdGuid);
|
||||
return user?.ToIdentityUser();
|
||||
}
|
||||
}
|
||||
}
|
53
src/Core/Identity/ReadOnlyEnvIdentityUserStore.cs
Normal file
53
src/Core/Identity/ReadOnlyEnvIdentityUserStore.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Bit.Core.Identity
|
||||
{
|
||||
public class ReadOnlyEnvIdentityUserStore : ReadOnlyIdentityUserStore
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public ReadOnlyEnvIdentityUserStore(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public override Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var usersCsv = _configuration["adminSettings:admins"];
|
||||
if(string.IsNullOrWhiteSpace(usersCsv))
|
||||
{
|
||||
return Task.FromResult<IdentityUser>(null);
|
||||
}
|
||||
|
||||
var users = usersCsv.ToLowerInvariant().Split(',');
|
||||
var user = users.Where(a => a.Trim() == normalizedEmail).FirstOrDefault();
|
||||
if(user == null || !user.Contains("@"))
|
||||
{
|
||||
return Task.FromResult<IdentityUser>(null);
|
||||
}
|
||||
|
||||
user = user.Trim();
|
||||
return Task.FromResult(new IdentityUser
|
||||
{
|
||||
Id = user,
|
||||
Email = user,
|
||||
NormalizedEmail = user,
|
||||
EmailConfirmed = true,
|
||||
UserName = user,
|
||||
NormalizedUserName = user,
|
||||
SecurityStamp = user
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return FindByEmailAsync(userId, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,22 +2,14 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.Identity
|
||||
{
|
||||
public class ReadOnlyIdentityUserStore :
|
||||
public abstract class ReadOnlyIdentityUserStore :
|
||||
IUserStore<IdentityUser>,
|
||||
IUserEmailStore<IdentityUser>,
|
||||
IUserSecurityStampStore<IdentityUser>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public ReadOnlyIdentityUserStore(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public Task<IdentityResult> CreateAsync(IdentityUser user,
|
||||
@ -32,24 +24,11 @@ namespace Bit.Core.Identity
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public async Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var user = await _userRepository.GetByEmailAsync(normalizedEmail);
|
||||
return user?.ToIdentityUser();
|
||||
}
|
||||
public abstract Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken));
|
||||
|
||||
public async Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
if(!Guid.TryParse(userId, out var userIdGuid))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(userIdGuid);
|
||||
return user?.ToIdentityUser();
|
||||
}
|
||||
public abstract Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default(CancellationToken));
|
||||
|
||||
public async Task<IdentityUser> FindByNameAsync(string normalizedUserName,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
|
@ -2,9 +2,9 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Billing.Utilities
|
||||
namespace Bit.Core.Utilities
|
||||
{
|
||||
public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute
|
||||
public class LoggingExceptionHandlerFilterAttribute : ExceptionFilterAttribute
|
||||
{
|
||||
public override void OnException(ExceptionContext context)
|
||||
{
|
||||
@ -15,7 +15,8 @@ namespace Bit.Billing.Utilities
|
||||
return;
|
||||
}
|
||||
|
||||
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ExceptionHandlerFilterAttribute>>();
|
||||
var logger = context.HttpContext.RequestServices
|
||||
.GetRequiredService<ILogger<LoggingExceptionHandlerFilterAttribute>>();
|
||||
logger.LogError(0, exception, exception.Message);
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -22,6 +21,7 @@ using System.IO;
|
||||
using SqlServerRepos = Bit.Core.Repositories.SqlServer;
|
||||
using System.Threading.Tasks;
|
||||
using TableStorageRepos = Bit.Core.Repositories.TableStorage;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Bit.Core.Utilities
|
||||
{
|
||||
@ -199,6 +199,38 @@ namespace Bit.Core.Utilities
|
||||
return identityBuilder;
|
||||
}
|
||||
|
||||
public static IdentityBuilder AddPasswordlessIdentityServices<TUserStore>(
|
||||
this IServiceCollection services, GlobalSettings globalSettings) where TUserStore : class
|
||||
{
|
||||
services.AddTransient<ILookupNormalizer, LowerInvariantLookupNormalizer>();
|
||||
|
||||
services.Configure<DataProtectionTokenProviderOptions>(options =>
|
||||
{
|
||||
options.TokenLifespan = TimeSpan.FromMinutes(15);
|
||||
});
|
||||
|
||||
var identityBuilder = services.AddIdentity<IdentityUser, Role>()
|
||||
.AddUserStore<TUserStore>()
|
||||
.AddRoleStore<RoleStore>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
services.TryAddScoped<PasswordlessSignInManager<IdentityUser>, PasswordlessSignInManager<IdentityUser>>();
|
||||
|
||||
services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/login";
|
||||
options.LogoutPath = "/";
|
||||
options.AccessDeniedPath = "/login?accessDenied=1";
|
||||
options.Cookie.Name = $"Bitwarden_{globalSettings.ProjectName}";
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
|
||||
options.ReturnUrlParameter = "returnUrl";
|
||||
options.SlidingExpiration = true;
|
||||
});
|
||||
|
||||
return identityBuilder;
|
||||
}
|
||||
|
||||
public static IIdentityServerBuilder AddCustomIdentityServerServices(
|
||||
this IServiceCollection services, IHostingEnvironment env, GlobalSettings globalSettings)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user