diff --git a/src/Api/IdentityServer/ApiResources.cs b/src/Api/IdentityServer/ApiResources.cs index eb9824121b..154f57aea4 100644 --- a/src/Api/IdentityServer/ApiResources.cs +++ b/src/Api/IdentityServer/ApiResources.cs @@ -16,12 +16,12 @@ namespace Bit.Api.IdentityServer ClaimTypes.Email, "securitystamp", - "nam", // name - "eml", // email - "sst", // security stamp - "pln", // plan - "tex", // trial expiration - "dev" // device identifier + "name", + "email", + "sstamp", // security stamp + "plan", + "trial_exp", + "device" }) }; } diff --git a/src/Api/IdentityServer/Clients.cs b/src/Api/IdentityServer/Clients.cs index 34020104a2..6ccd578551 100644 --- a/src/Api/IdentityServer/Clients.cs +++ b/src/Api/IdentityServer/Clients.cs @@ -23,6 +23,8 @@ namespace Bit.Api.IdentityServer ClientId = id; RequireClientSecret = false; AllowedGrantTypes = GrantTypes.ResourceOwnerPassword; + UpdateAccessTokenClaimsOnRefresh = true; + AccessTokenLifetime = 60 * 60; // 1 hour AllowOfflineAccess = true; AllowedScopes = new string[] { "api" }; } diff --git a/src/Api/IdentityServer/ProfileService.cs b/src/Api/IdentityServer/ProfileService.cs index 6f1f1c8393..aabcbe076a 100644 --- a/src/Api/IdentityServer/ProfileService.cs +++ b/src/Api/IdentityServer/ProfileService.cs @@ -3,6 +3,13 @@ using System.Threading.Tasks; using IdentityServer4.Models; using Bit.Core.Repositories; 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 { @@ -10,25 +17,73 @@ namespace Bit.Api.IdentityServer { private readonly IUserService _userService; private readonly IUserRepository _userRepository; + private IdentityOptions _identityOptions; public ProfileService( IUserRepository userRepository, - IUserService userService) + IUserService userService, + IOptions identityOptionsAccessor) { _userRepository = userRepository; _userService = userService; + _identityOptions = identityOptionsAccessor?.Value ?? new IdentityOptions(); } - public Task GetProfileDataAsync(ProfileDataRequestContext context) + public async Task GetProfileDataAsync(ProfileDataRequestContext context) { - context.AddFilteredClaims(context.Subject.Claims); - return Task.FromResult(0); + var claims = context.Subject.Claims.ToList(); + var user = await GetUserAsync(context.Subject); + if(user != null) + { + claims.AddRange(new List { + 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; - return Task.FromResult(0); + var securityTokenClaim = context.Subject?.Claims.FirstOrDefault(c => + 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 GetUserAsync(ClaimsPrincipal principal) + { + var userId = _userService.GetProperUserId(principal); + if(userId.HasValue) + { + return await _userService.GetUserByIdAsync(userId.Value); + } + + return null; } } } diff --git a/src/Api/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Api/IdentityServer/ResourceOwnerPasswordValidator.cs index 0a76acdae1..b60e5ca928 100644 --- a/src/Api/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Api/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -42,11 +42,10 @@ namespace Bit.Api.IdentityServer { Init(); - var oldAuthBearer = context.Request.Raw["oldAuthBearer"]?.ToString(); - var twoFactorCode = context.Request.Raw["twoFactorCode"]?.ToString(); - var twoFactorProvider = context.Request.Raw["twoFactorProvider"]?.ToString(); - var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorCode) && - !string.IsNullOrWhiteSpace(twoFactorProvider); + var oldAuthBearer = context.Request.Raw["OldAuthBearer"]?.ToString(); + var twoFactorToken = context.Request.Raw["TwoFactorToken"]?.ToString(); + var twoFactorProvider = context.Request.Raw["TwoFactorProvider"]?.ToString(); + var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && !string.IsNullOrWhiteSpace(twoFactorProvider); if(!string.IsNullOrWhiteSpace(oldAuthBearer) && _jwtBearerOptions != null) { @@ -80,11 +79,11 @@ namespace Bit.Api.IdentityServer context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Two factor required.", new Dictionary { { "TwoFactorRequired", true }, - { "TwoFactorProvider", ((int?)user.TwoFactorProvider)?.ToString() } }); + { "TwoFactorProviders", new string[] { ((int?)user.TwoFactorProvider)?.ToString() } } }); return; } - if(!twoFactorRequest || await _userManager.VerifyTwoFactorTokenAsync(user, twoFactorProvider, twoFactorCode)) + if(!twoFactorRequest || await _userManager.VerifyTwoFactorTokenAsync(user, twoFactorProvider, twoFactorToken)) { var device = await SaveDeviceAsync(user, context); BuildSuccessResult(user, context, device); @@ -114,29 +113,18 @@ namespace Bit.Api.IdentityServer private void BuildSuccessResult(User user, ResourceOwnerPasswordValidationContext context, Device device) { var claims = new List { - new Claim("pln", "0"), // free plan - new Claim("sst", user.SecurityStamp), - new Claim("eml", user.Email), - - // 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) + // Deprecated claims for backwards compatability + new Claim(ClaimTypes.AuthenticationMethod, "Application"), + new Claim(_identityOptions.ClaimsIdentity.UserIdClaimType, user.Id.ToString()) }; if(device != null) { - claims.Add(new Claim("dev", device.Identifier)); - } - - if(!string.IsNullOrWhiteSpace(user.Name)) - { - claims.Add(new Claim("nam", user.Name)); + claims.Add(new Claim("device", device.Identifier)); } context.Result = new GrantValidationResult(user.Id.ToString(), "Application", identityProvider: "bitwarden", - claims: claims); + claims: claims.Count > 0 ? claims : null); } private AuthenticationTicket ValidateOldAuthBearer(string token) diff --git a/src/Api/Middleware/CurrentContextMiddleware.cs b/src/Api/Middleware/CurrentContextMiddleware.cs new file mode 100644 index 0000000000..61153db4d0 --- /dev/null +++ b/src/Api/Middleware/CurrentContextMiddleware.cs @@ -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); + } + } +} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 430c3b83b2..6d6498e220 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -268,6 +268,9 @@ namespace Bit.Api // Add Jwt authentication to the request pipeline. app.UseJwtBearerIdentity(); + // Add current context + app.UseMiddleware(); + // Add MVC to the request pipeline. app.UseMvc(); } diff --git a/src/Core/Identity/JwtBearerEventImplementations.cs b/src/Core/Identity/JwtBearerEventImplementations.cs index beebe465c6..a84a9529d6 100644 --- a/src/Core/Identity/JwtBearerEventImplementations.cs +++ b/src/Core/Identity/JwtBearerEventImplementations.cs @@ -2,12 +2,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.DependencyInjection; -using Bit.Core.Repositories; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.IdentityModel.Tokens; -using Microsoft.AspNetCore.Identity; -using Bit.Core.Domains; +using Bit.Core.Services; namespace Bit.Core.Identity { @@ -20,26 +18,17 @@ namespace Bit.Core.Identity throw new InvalidOperationException("RequestServices is null"); } - var userRepository = context.HttpContext.RequestServices.GetRequiredService(); - var userManager = context.HttpContext.RequestServices.GetRequiredService>(); + var userService = context.HttpContext.RequestServices.GetRequiredService(); var signInManager = context.HttpContext.RequestServices.GetRequiredService(); - var userId = userManager.GetUserId(context.Ticket.Principal); - var user = await userRepository.GetByIdAsync(new Guid(userId)); + var userId = userService.GetProperUserId(context.Ticket.Principal); + var user = await userService.GetUserByIdAsync(userId.Value); // validate security token if(!await signInManager.ValidateSecurityStampAsync(user, context.Ticket.Principal)) { throw new SecurityTokenValidationException("Bad security stamp."); } - - // register the current context user - var currentContext = context.HttpContext.RequestServices.GetRequiredService(); - 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) diff --git a/src/Core/Identity/UserStore.cs b/src/Core/Identity/UserStore.cs index 13d574559c..ce45f57dec 100644 --- a/src/Core/Identity/UserStore.cs +++ b/src/Core/Identity/UserStore.cs @@ -50,29 +50,31 @@ namespace Bit.Core.Identity return _currentContext.User; } - return await _userRepository.GetByEmailAsync(normalizedEmail); + _currentContext.User = await _userRepository.GetByEmailAsync(normalizedEmail); + return _currentContext.User; } public async Task FindByIdAsync(string userId, CancellationToken cancellationToken = default(CancellationToken)) { - var id = new Guid(userId); - - if(_currentContext?.User != null && _currentContext.User.Id == id) + if(_currentContext?.User != null && + string.Equals(_currentContext.User.Id.ToString(), userId, StringComparison.InvariantCultureIgnoreCase)) { 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 FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken = default(CancellationToken)) { - if(_currentContext?.User != null && _currentContext.User.Email == normalizedUserName) - { - return _currentContext.User; - } - - return await _userRepository.GetByEmailAsync(normalizedUserName); + return await FindByEmailAsync(normalizedUserName, cancellationToken); } public Task GetEmailAsync(User user, CancellationToken cancellationToken = default(CancellationToken)) diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 35b45148ba..c953fc070e 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -23,6 +23,7 @@ namespace Bit.Core.Services private readonly IdentityOptions _identityOptions; private readonly IPasswordHasher _passwordHasher; private readonly IEnumerable> _passwordValidators; + private readonly CurrentContext _currentContext; public UserService( IUserRepository userRepository, @@ -36,7 +37,8 @@ namespace Bit.Core.Services ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, - ILogger> logger) + ILogger> logger, + CurrentContext currentContext) : base( store, optionsAccessor, @@ -55,6 +57,7 @@ namespace Bit.Core.Services _identityErrorDescriber = errors; _passwordHasher = passwordHasher; _passwordValidators = passwordValidators; + _currentContext = currentContext; } public Guid? GetProperUserId(ClaimsPrincipal principal) @@ -70,18 +73,31 @@ namespace Bit.Core.Services public async Task GetUserByIdAsync(string userId) { + if(_currentContext?.User != null && + string.Equals(_currentContext.User.Id.ToString(), userId, StringComparison.InvariantCultureIgnoreCase)) + { + return _currentContext.User; + } + Guid userIdGuid; if(!Guid.TryParse(userId, out userIdGuid)) { return null; } - return await _userRepository.GetByIdAsync(userIdGuid); + _currentContext.User = await _userRepository.GetByIdAsync(userIdGuid); + return _currentContext.User; } public async Task 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 GetAccountRevisionDateByIdAsync(Guid userId)