mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
[SM-220] Move identity specific files to identity (#2279)
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Admin.IdentityServer;
|
||||
using Bit.Admin.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
87
src/Admin/IdentityServer/PasswordlessSignInManager.cs
Normal file
87
src/Admin/IdentityServer/PasswordlessSignInManager.cs
Normal file
@ -0,0 +1,87 @@
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Admin.IdentityServer;
|
||||
|
||||
public class PasswordlessSignInManager<TUser> : SignInManager<TUser> where TUser : class
|
||||
{
|
||||
public const string PasswordlessSignInPurpose = "PasswordlessSignIn";
|
||||
|
||||
private readonly IMailService _mailService;
|
||||
|
||||
public PasswordlessSignInManager(UserManager<TUser> userManager,
|
||||
IHttpContextAccessor contextAccessor,
|
||||
IUserClaimsPrincipalFactory<TUser> claimsFactory,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
ILogger<SignInManager<TUser>> logger,
|
||||
IAuthenticationSchemeProvider schemes,
|
||||
IUserConfirmation<TUser> confirmation,
|
||||
IMailService mailService)
|
||||
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
|
||||
{
|
||||
_mailService = mailService;
|
||||
}
|
||||
|
||||
public async Task<SignInResult> PasswordlessSignInAsync(string email, string returnUrl)
|
||||
{
|
||||
var user = await UserManager.FindByEmailAsync(email);
|
||||
if (user == null)
|
||||
{
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
var token = await UserManager.GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
|
||||
PasswordlessSignInPurpose);
|
||||
await _mailService.SendPasswordlessSignInAsync(returnUrl, token, email);
|
||||
return SignInResult.Success;
|
||||
}
|
||||
|
||||
public async Task<SignInResult> PasswordlessSignInAsync(TUser user, string token, bool isPersistent)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
var attempt = await CheckPasswordlessSignInAsync(user, token);
|
||||
return attempt.Succeeded ?
|
||||
await SignInOrTwoFactorAsync(user, isPersistent, bypassTwoFactor: true) : attempt;
|
||||
}
|
||||
|
||||
public async Task<SignInResult> PasswordlessSignInAsync(string email, string token, bool isPersistent)
|
||||
{
|
||||
var user = await UserManager.FindByEmailAsync(email);
|
||||
if (user == null)
|
||||
{
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
return await PasswordlessSignInAsync(user, token, isPersistent);
|
||||
}
|
||||
|
||||
public virtual async Task<SignInResult> CheckPasswordlessSignInAsync(TUser 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,
|
||||
PasswordlessSignInPurpose, token))
|
||||
{
|
||||
return SignInResult.Success;
|
||||
}
|
||||
|
||||
Logger.LogWarning(2, "User {userId} failed to provide the correct token.",
|
||||
await UserManager.GetUserIdAsync(user));
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
}
|
65
src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs
Normal file
65
src/Admin/IdentityServer/ReadOnlyEnvIdentityUserStore.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Admin.IdentityServer;
|
||||
|
||||
public class ReadOnlyEnvIdentityUserStore : ReadOnlyIdentityUserStore
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public ReadOnlyEnvIdentityUserStore(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public override Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var usersCsv = _configuration["adminSettings:admins"];
|
||||
if (!CoreHelpers.SettingHasValue(usersCsv))
|
||||
{
|
||||
return Task.FromResult<IdentityUser>(null);
|
||||
}
|
||||
|
||||
var users = usersCsv.ToLowerInvariant().Split(',');
|
||||
var usersDict = new Dictionary<string, string>();
|
||||
foreach (var u in users)
|
||||
{
|
||||
var parts = u.Split(':');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var email = parts[0].Trim();
|
||||
var stamp = parts[1].Trim();
|
||||
usersDict.Add(email, stamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
var email = parts[0].Trim();
|
||||
usersDict.Add(email, email);
|
||||
}
|
||||
}
|
||||
|
||||
var userStamp = usersDict.ContainsKey(normalizedEmail) ? usersDict[normalizedEmail] : null;
|
||||
if (userStamp == null)
|
||||
{
|
||||
return Task.FromResult<IdentityUser>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult(new IdentityUser
|
||||
{
|
||||
Id = normalizedEmail,
|
||||
Email = normalizedEmail,
|
||||
NormalizedEmail = normalizedEmail,
|
||||
EmailConfirmed = true,
|
||||
UserName = normalizedEmail,
|
||||
NormalizedUserName = normalizedEmail,
|
||||
SecurityStamp = userStamp
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return FindByEmailAsync(userId, cancellationToken);
|
||||
}
|
||||
}
|
118
src/Admin/IdentityServer/ReadOnlyIdentityUserStore.cs
Normal file
118
src/Admin/IdentityServer/ReadOnlyIdentityUserStore.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Admin.IdentityServer;
|
||||
|
||||
public abstract class ReadOnlyIdentityUserStore :
|
||||
IUserEmailStore<IdentityUser>,
|
||||
IUserSecurityStampStore<IdentityUser>
|
||||
{
|
||||
public void Dispose() { }
|
||||
|
||||
public Task<IdentityResult> CreateAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IdentityResult> DeleteAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public abstract Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public abstract Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public async Task<IdentityUser> FindByNameAsync(string normalizedUserName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await FindByEmailAsync(normalizedUserName, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string> GetEmailAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<bool> GetEmailConfirmedAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.EmailConfirmed);
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedEmailAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedUserNameAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<string> GetUserIdAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.Id);
|
||||
}
|
||||
|
||||
public Task<string> GetUserNameAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task SetEmailAsync(IdentityUser user, string email,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
user.NormalizedEmail = normalizedEmail;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
user.NormalizedUserName = normalizedName;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetUserNameAsync(IdentityUser user, string userName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IdentityResult> UpdateAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
|
||||
public Task SetSecurityStampAsync(IdentityUser user, string stamp, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.FromResult(user.SecurityStamp);
|
||||
}
|
||||
}
|
44
src/Admin/IdentityServer/ServiceCollectionExtensions.cs
Normal file
44
src/Admin/IdentityServer/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Bit.Admin.IdentityServer;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static Tuple<IdentityBuilder, IdentityBuilder> AddPasswordlessIdentityServices<TUserStore>(
|
||||
this IServiceCollection services, GlobalSettings globalSettings) where TUserStore : class
|
||||
{
|
||||
services.TryAddTransient<ILookupNormalizer, LowerInvariantLookupNormalizer>();
|
||||
services.Configure<DataProtectionTokenProviderOptions>(options =>
|
||||
{
|
||||
options.TokenLifespan = TimeSpan.FromMinutes(15);
|
||||
});
|
||||
|
||||
var passwordlessIdentityBuilder = services.AddIdentity<IdentityUser, Role>()
|
||||
.AddUserStore<TUserStore>()
|
||||
.AddRoleStore<RoleStore>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
var regularIdentityBuilder = services.AddIdentityCore<User>()
|
||||
.AddUserStore<UserStore>();
|
||||
|
||||
services.TryAddScoped<PasswordlessSignInManager<IdentityUser>, PasswordlessSignInManager<IdentityUser>>();
|
||||
|
||||
services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/login";
|
||||
options.LogoutPath = "/";
|
||||
options.AccessDeniedPath = "/login?accessDenied=true";
|
||||
options.Cookie.Name = $"Bitwarden_{globalSettings.ProjectName}";
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.ExpireTimeSpan = TimeSpan.FromDays(2);
|
||||
options.ReturnUrlParameter = "returnUrl";
|
||||
options.SlidingExpiration = true;
|
||||
});
|
||||
|
||||
return new Tuple<IdentityBuilder, IdentityBuilder>(passwordlessIdentityBuilder, regularIdentityBuilder);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using System.Globalization;
|
||||
using Bit.Admin.IdentityServer;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
Reference in New Issue
Block a user