mirror of
https://github.com/bitwarden/server.git
synced 2025-05-28 23:04:50 -05:00
Move claims issuance and security stamp checks out into profile service. moved context sets out of identity implementations and into get methods.
This commit is contained in:
parent
cb5419aca8
commit
8a83600e52
@ -16,12 +16,12 @@ namespace Bit.Api.IdentityServer
|
|||||||
ClaimTypes.Email,
|
ClaimTypes.Email,
|
||||||
"securitystamp",
|
"securitystamp",
|
||||||
|
|
||||||
"nam", // name
|
"name",
|
||||||
"eml", // email
|
"email",
|
||||||
"sst", // security stamp
|
"sstamp", // security stamp
|
||||||
"pln", // plan
|
"plan",
|
||||||
"tex", // trial expiration
|
"trial_exp",
|
||||||
"dev" // device identifier
|
"device"
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ namespace Bit.Api.IdentityServer
|
|||||||
ClientId = id;
|
ClientId = id;
|
||||||
RequireClientSecret = false;
|
RequireClientSecret = false;
|
||||||
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword;
|
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword;
|
||||||
|
UpdateAccessTokenClaimsOnRefresh = true;
|
||||||
|
AccessTokenLifetime = 60 * 60; // 1 hour
|
||||||
AllowOfflineAccess = true;
|
AllowOfflineAccess = true;
|
||||||
AllowedScopes = new string[] { "api" };
|
AllowedScopes = new string[] { "api" };
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,13 @@ using System.Threading.Tasks;
|
|||||||
using IdentityServer4.Models;
|
using IdentityServer4.Models;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Bit.Core.Domains;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace Bit.Api.IdentityServer
|
namespace Bit.Api.IdentityServer
|
||||||
{
|
{
|
||||||
@ -10,25 +17,73 @@ namespace Bit.Api.IdentityServer
|
|||||||
{
|
{
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
|
private IdentityOptions _identityOptions;
|
||||||
|
|
||||||
public ProfileService(
|
public ProfileService(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IUserService userService)
|
IUserService userService,
|
||||||
|
IOptions<IdentityOptions> identityOptionsAccessor)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_identityOptions = identityOptionsAccessor?.Value ?? new IdentityOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task GetProfileDataAsync(ProfileDataRequestContext context)
|
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
|
||||||
{
|
{
|
||||||
context.AddFilteredClaims(context.Subject.Claims);
|
var claims = context.Subject.Claims.ToList();
|
||||||
return Task.FromResult(0);
|
var user = await GetUserAsync(context.Subject);
|
||||||
|
if(user != null)
|
||||||
|
{
|
||||||
|
claims.AddRange(new List<Claim> {
|
||||||
|
new Claim("plan", "0"), // free plan hard coded for now
|
||||||
|
new Claim("sstamp", user.SecurityStamp),
|
||||||
|
new Claim("email", user.Email),
|
||||||
|
|
||||||
|
// Deprecated claims for backwards compatability,
|
||||||
|
new Claim(_identityOptions.ClaimsIdentity.UserNameClaimType, user.Email),
|
||||||
|
new Claim(_identityOptions.ClaimsIdentity.SecurityStampClaimType, user.SecurityStamp)
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!string.IsNullOrWhiteSpace(user.Name))
|
||||||
|
{
|
||||||
|
claims.Add(new Claim("name", user.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(claims.Count > 0)
|
||||||
|
{
|
||||||
|
context.AddFilteredClaims(claims);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task IsActiveAsync(IsActiveContext context)
|
public async Task IsActiveAsync(IsActiveContext context)
|
||||||
{
|
{
|
||||||
context.IsActive = true;
|
var securityTokenClaim = context.Subject?.Claims.FirstOrDefault(c =>
|
||||||
return Task.FromResult(0);
|
c.Type == _identityOptions.ClaimsIdentity.SecurityStampClaimType);
|
||||||
|
var user = await GetUserAsync(context.Subject);
|
||||||
|
|
||||||
|
if(user != null && securityTokenClaim != null)
|
||||||
|
{
|
||||||
|
context.IsActive = string.Equals(user.SecurityStamp, securityTokenClaim.Value,
|
||||||
|
StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.IsActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<User> GetUserAsync(ClaimsPrincipal principal)
|
||||||
|
{
|
||||||
|
var userId = _userService.GetProperUserId(principal);
|
||||||
|
if(userId.HasValue)
|
||||||
|
{
|
||||||
|
return await _userService.GetUserByIdAsync(userId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,11 +42,10 @@ namespace Bit.Api.IdentityServer
|
|||||||
{
|
{
|
||||||
Init();
|
Init();
|
||||||
|
|
||||||
var oldAuthBearer = context.Request.Raw["oldAuthBearer"]?.ToString();
|
var oldAuthBearer = context.Request.Raw["OldAuthBearer"]?.ToString();
|
||||||
var twoFactorCode = context.Request.Raw["twoFactorCode"]?.ToString();
|
var twoFactorToken = context.Request.Raw["TwoFactorToken"]?.ToString();
|
||||||
var twoFactorProvider = context.Request.Raw["twoFactorProvider"]?.ToString();
|
var twoFactorProvider = context.Request.Raw["TwoFactorProvider"]?.ToString();
|
||||||
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorCode) &&
|
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && !string.IsNullOrWhiteSpace(twoFactorProvider);
|
||||||
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
|
||||||
|
|
||||||
if(!string.IsNullOrWhiteSpace(oldAuthBearer) && _jwtBearerOptions != null)
|
if(!string.IsNullOrWhiteSpace(oldAuthBearer) && _jwtBearerOptions != null)
|
||||||
{
|
{
|
||||||
@ -80,11 +79,11 @@ namespace Bit.Api.IdentityServer
|
|||||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Two factor required.",
|
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Two factor required.",
|
||||||
new Dictionary<string, object> {
|
new Dictionary<string, object> {
|
||||||
{ "TwoFactorRequired", true },
|
{ "TwoFactorRequired", true },
|
||||||
{ "TwoFactorProvider", ((int?)user.TwoFactorProvider)?.ToString() } });
|
{ "TwoFactorProviders", new string[] { ((int?)user.TwoFactorProvider)?.ToString() } } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!twoFactorRequest || await _userManager.VerifyTwoFactorTokenAsync(user, twoFactorProvider, twoFactorCode))
|
if(!twoFactorRequest || await _userManager.VerifyTwoFactorTokenAsync(user, twoFactorProvider, twoFactorToken))
|
||||||
{
|
{
|
||||||
var device = await SaveDeviceAsync(user, context);
|
var device = await SaveDeviceAsync(user, context);
|
||||||
BuildSuccessResult(user, context, device);
|
BuildSuccessResult(user, context, device);
|
||||||
@ -114,29 +113,18 @@ namespace Bit.Api.IdentityServer
|
|||||||
private void BuildSuccessResult(User user, ResourceOwnerPasswordValidationContext context, Device device)
|
private void BuildSuccessResult(User user, ResourceOwnerPasswordValidationContext context, Device device)
|
||||||
{
|
{
|
||||||
var claims = new List<Claim> {
|
var claims = new List<Claim> {
|
||||||
new Claim("pln", "0"), // free plan
|
// Deprecated claims for backwards compatability
|
||||||
new Claim("sst", user.SecurityStamp),
|
new Claim(ClaimTypes.AuthenticationMethod, "Application"),
|
||||||
new Claim("eml", user.Email),
|
new Claim(_identityOptions.ClaimsIdentity.UserIdClaimType, user.Id.ToString())
|
||||||
|
|
||||||
// Deprecated claims for backwards compatability
|
|
||||||
new Claim(ClaimTypes.AuthenticationMethod, "Application"),
|
|
||||||
new Claim(_identityOptions.ClaimsIdentity.UserIdClaimType, user.Id.ToString()),
|
|
||||||
new Claim(_identityOptions.ClaimsIdentity.UserNameClaimType, user.Email),
|
|
||||||
new Claim(_identityOptions.ClaimsIdentity.SecurityStampClaimType, user.SecurityStamp)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if(device != null)
|
if(device != null)
|
||||||
{
|
{
|
||||||
claims.Add(new Claim("dev", device.Identifier));
|
claims.Add(new Claim("device", device.Identifier));
|
||||||
}
|
|
||||||
|
|
||||||
if(!string.IsNullOrWhiteSpace(user.Name))
|
|
||||||
{
|
|
||||||
claims.Add(new Claim("nam", user.Name));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.Result = new GrantValidationResult(user.Id.ToString(), "Application", identityProvider: "bitwarden",
|
context.Result = new GrantValidationResult(user.Id.ToString(), "Application", identityProvider: "bitwarden",
|
||||||
claims: claims);
|
claims: claims.Count > 0 ? claims : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthenticationTicket ValidateOldAuthBearer(string token)
|
private AuthenticationTicket ValidateOldAuthBearer(string token)
|
||||||
|
33
src/Api/Middleware/CurrentContextMiddleware.cs
Normal file
33
src/Api/Middleware/CurrentContextMiddleware.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using Bit.Core;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Bit.Api.Middleware
|
||||||
|
{
|
||||||
|
public class CurrentContextMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
|
public CurrentContextMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Invoke(HttpContext httpContext, CurrentContext currentContext)
|
||||||
|
{
|
||||||
|
if(httpContext.User != null)
|
||||||
|
{
|
||||||
|
var securityStampClaim = httpContext.User.Claims.FirstOrDefault(c => c.Type == "device");
|
||||||
|
currentContext.DeviceIdentifier = securityStampClaim?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(currentContext.DeviceIdentifier == null && httpContext.Request.Headers.ContainsKey("Device-Identifier"))
|
||||||
|
{
|
||||||
|
currentContext.DeviceIdentifier = httpContext.Request.Headers["Device-Identifier"];
|
||||||
|
}
|
||||||
|
|
||||||
|
await _next.Invoke(httpContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -268,6 +268,9 @@ namespace Bit.Api
|
|||||||
// Add Jwt authentication to the request pipeline.
|
// Add Jwt authentication to the request pipeline.
|
||||||
app.UseJwtBearerIdentity();
|
app.UseJwtBearerIdentity();
|
||||||
|
|
||||||
|
// Add current context
|
||||||
|
app.UseMiddleware<CurrentContextMiddleware>();
|
||||||
|
|
||||||
// Add MVC to the request pipeline.
|
// Add MVC to the request pipeline.
|
||||||
app.UseMvc();
|
app.UseMvc();
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,10 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Http.Authentication;
|
using Microsoft.AspNetCore.Http.Authentication;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Domains;
|
|
||||||
|
|
||||||
namespace Bit.Core.Identity
|
namespace Bit.Core.Identity
|
||||||
{
|
{
|
||||||
@ -20,26 +18,17 @@ namespace Bit.Core.Identity
|
|||||||
throw new InvalidOperationException("RequestServices is null");
|
throw new InvalidOperationException("RequestServices is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
var userRepository = context.HttpContext.RequestServices.GetRequiredService<IUserRepository>();
|
var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
|
||||||
var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<User>>();
|
|
||||||
var signInManager = context.HttpContext.RequestServices.GetRequiredService<JwtBearerSignInManager>();
|
var signInManager = context.HttpContext.RequestServices.GetRequiredService<JwtBearerSignInManager>();
|
||||||
|
|
||||||
var userId = userManager.GetUserId(context.Ticket.Principal);
|
var userId = userService.GetProperUserId(context.Ticket.Principal);
|
||||||
var user = await userRepository.GetByIdAsync(new Guid(userId));
|
var user = await userService.GetUserByIdAsync(userId.Value);
|
||||||
|
|
||||||
// validate security token
|
// validate security token
|
||||||
if(!await signInManager.ValidateSecurityStampAsync(user, context.Ticket.Principal))
|
if(!await signInManager.ValidateSecurityStampAsync(user, context.Ticket.Principal))
|
||||||
{
|
{
|
||||||
throw new SecurityTokenValidationException("Bad security stamp.");
|
throw new SecurityTokenValidationException("Bad security stamp.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// register the current context user
|
|
||||||
var currentContext = context.HttpContext.RequestServices.GetRequiredService<CurrentContext>();
|
|
||||||
currentContext.User = user;
|
|
||||||
if(context.HttpContext.Request.Headers.ContainsKey("Device-Identifier"))
|
|
||||||
{
|
|
||||||
currentContext.DeviceIdentifier = context.HttpContext.Request.Headers["Device-Identifier"];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task AuthenticationFailedAsync(AuthenticationFailedContext context)
|
public static Task AuthenticationFailedAsync(AuthenticationFailedContext context)
|
||||||
|
@ -50,29 +50,31 @@ namespace Bit.Core.Identity
|
|||||||
return _currentContext.User;
|
return _currentContext.User;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _userRepository.GetByEmailAsync(normalizedEmail);
|
_currentContext.User = await _userRepository.GetByEmailAsync(normalizedEmail);
|
||||||
|
return _currentContext.User;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
|
public async Task<User> FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
{
|
{
|
||||||
var id = new Guid(userId);
|
if(_currentContext?.User != null &&
|
||||||
|
string.Equals(_currentContext.User.Id.ToString(), userId, StringComparison.InvariantCultureIgnoreCase))
|
||||||
if(_currentContext?.User != null && _currentContext.User.Id == id)
|
|
||||||
{
|
{
|
||||||
return _currentContext.User;
|
return _currentContext.User;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _userRepository.GetByIdAsync(id);
|
Guid userIdGuid;
|
||||||
|
if(!Guid.TryParse(userId, out userIdGuid))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentContext.User = await _userRepository.GetByIdAsync(userIdGuid);
|
||||||
|
return _currentContext.User;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))
|
public async Task<User> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
{
|
{
|
||||||
if(_currentContext?.User != null && _currentContext.User.Email == normalizedUserName)
|
return await FindByEmailAsync(normalizedUserName, cancellationToken);
|
||||||
{
|
|
||||||
return _currentContext.User;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await _userRepository.GetByEmailAsync(normalizedUserName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> GetEmailAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
|
public Task<string> GetEmailAsync(User user, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
@ -23,6 +23,7 @@ namespace Bit.Core.Services
|
|||||||
private readonly IdentityOptions _identityOptions;
|
private readonly IdentityOptions _identityOptions;
|
||||||
private readonly IPasswordHasher<User> _passwordHasher;
|
private readonly IPasswordHasher<User> _passwordHasher;
|
||||||
private readonly IEnumerable<IPasswordValidator<User>> _passwordValidators;
|
private readonly IEnumerable<IPasswordValidator<User>> _passwordValidators;
|
||||||
|
private readonly CurrentContext _currentContext;
|
||||||
|
|
||||||
public UserService(
|
public UserService(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
@ -36,7 +37,8 @@ namespace Bit.Core.Services
|
|||||||
ILookupNormalizer keyNormalizer,
|
ILookupNormalizer keyNormalizer,
|
||||||
IdentityErrorDescriber errors,
|
IdentityErrorDescriber errors,
|
||||||
IServiceProvider services,
|
IServiceProvider services,
|
||||||
ILogger<UserManager<User>> logger)
|
ILogger<UserManager<User>> logger,
|
||||||
|
CurrentContext currentContext)
|
||||||
: base(
|
: base(
|
||||||
store,
|
store,
|
||||||
optionsAccessor,
|
optionsAccessor,
|
||||||
@ -55,6 +57,7 @@ namespace Bit.Core.Services
|
|||||||
_identityErrorDescriber = errors;
|
_identityErrorDescriber = errors;
|
||||||
_passwordHasher = passwordHasher;
|
_passwordHasher = passwordHasher;
|
||||||
_passwordValidators = passwordValidators;
|
_passwordValidators = passwordValidators;
|
||||||
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
||||||
@ -70,18 +73,31 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
public async Task<User> GetUserByIdAsync(string userId)
|
public async Task<User> GetUserByIdAsync(string userId)
|
||||||
{
|
{
|
||||||
|
if(_currentContext?.User != null &&
|
||||||
|
string.Equals(_currentContext.User.Id.ToString(), userId, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
return _currentContext.User;
|
||||||
|
}
|
||||||
|
|
||||||
Guid userIdGuid;
|
Guid userIdGuid;
|
||||||
if(!Guid.TryParse(userId, out userIdGuid))
|
if(!Guid.TryParse(userId, out userIdGuid))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _userRepository.GetByIdAsync(userIdGuid);
|
_currentContext.User = await _userRepository.GetByIdAsync(userIdGuid);
|
||||||
|
return _currentContext.User;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<User> GetUserByIdAsync(Guid userId)
|
public async Task<User> GetUserByIdAsync(Guid userId)
|
||||||
{
|
{
|
||||||
return await _userRepository.GetByIdAsync(userId);
|
if(_currentContext?.User != null && _currentContext.User.Id == userId)
|
||||||
|
{
|
||||||
|
return _currentContext.User;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentContext.User = await _userRepository.GetByIdAsync(userId);
|
||||||
|
return _currentContext.User;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DateTime> GetAccountRevisionDateByIdAsync(Guid userId)
|
public async Task<DateTime> GetAccountRevisionDateByIdAsync(Guid userId)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user