mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
Move idenityserver implementations into API project
This commit is contained in:
22
src/Api/IdentityServer/ApiResources.cs
Normal file
22
src/Api/IdentityServer/ApiResources.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using IdentityServer4.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Bit.Api.IdentityServer
|
||||
{
|
||||
public class ApiResources
|
||||
{
|
||||
public static IEnumerable<ApiResource> GetApiResources()
|
||||
{
|
||||
return new List<ApiResource>
|
||||
{
|
||||
new ApiResource("api", "Vault API", new string[] {
|
||||
ClaimTypes.AuthenticationMethod,
|
||||
ClaimTypes.NameIdentifier,
|
||||
ClaimTypes.Email,
|
||||
"securitystamp"
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
31
src/Api/IdentityServer/Clients.cs
Normal file
31
src/Api/IdentityServer/Clients.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using IdentityServer4.Models;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.Api.IdentityServer
|
||||
{
|
||||
public class Clients
|
||||
{
|
||||
public static IEnumerable<Client> GetClients()
|
||||
{
|
||||
return new List<Client>
|
||||
{
|
||||
new ApiClient("mobile"),
|
||||
new ApiClient("web"),
|
||||
new ApiClient("browser"),
|
||||
new ApiClient("desktop")
|
||||
};
|
||||
}
|
||||
|
||||
public class ApiClient : Client
|
||||
{
|
||||
public ApiClient(string id)
|
||||
{
|
||||
ClientId = id;
|
||||
RequireClientSecret = false;
|
||||
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword;
|
||||
AllowOfflineAccess = true;
|
||||
AllowedScopes = new string[] { "api" };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
90
src/Api/IdentityServer/PersistedGrantStore.cs
Normal file
90
src/Api/IdentityServer/PersistedGrantStore.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Domains;
|
||||
using Bit.Core.Repositories;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Stores;
|
||||
|
||||
namespace Bit.Api.IdentityServer
|
||||
{
|
||||
public class PersistedGrantStore : IPersistedGrantStore
|
||||
{
|
||||
private readonly IGrantRepository _grantRepository;
|
||||
|
||||
public PersistedGrantStore(
|
||||
IGrantRepository grantRepository)
|
||||
{
|
||||
_grantRepository = grantRepository;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId)
|
||||
{
|
||||
var grants = await _grantRepository.GetManyAsync(subjectId);
|
||||
var pGrants = grants.Select(g => ToPersistedGrant(g));
|
||||
return pGrants;
|
||||
}
|
||||
|
||||
public async Task<PersistedGrant> GetAsync(string key)
|
||||
{
|
||||
var grant = await _grantRepository.GetByKeyAsync(key);
|
||||
if(grant == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var pGrant = ToPersistedGrant(grant);
|
||||
return pGrant;
|
||||
}
|
||||
|
||||
public async Task RemoveAllAsync(string subjectId, string clientId)
|
||||
{
|
||||
await _grantRepository.DeleteAsync(subjectId, clientId);
|
||||
}
|
||||
|
||||
public async Task RemoveAllAsync(string subjectId, string clientId, string type)
|
||||
{
|
||||
await _grantRepository.DeleteAsync(subjectId, clientId, type);
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(string key)
|
||||
{
|
||||
await _grantRepository.DeleteAsync(key);
|
||||
}
|
||||
|
||||
public async Task StoreAsync(PersistedGrant pGrant)
|
||||
{
|
||||
var grant = ToGrant(pGrant);
|
||||
await _grantRepository.SaveAsync(grant);
|
||||
}
|
||||
|
||||
private Grant ToGrant(PersistedGrant pGrant)
|
||||
{
|
||||
return new Grant
|
||||
{
|
||||
Key = pGrant.Key,
|
||||
Type = pGrant.Type,
|
||||
SubjectId = pGrant.SubjectId,
|
||||
ClientId = pGrant.ClientId,
|
||||
CreationDate = pGrant.CreationTime,
|
||||
ExpirationDate = pGrant.Expiration,
|
||||
Data = pGrant.Data
|
||||
};
|
||||
}
|
||||
|
||||
private PersistedGrant ToPersistedGrant(Grant grant)
|
||||
{
|
||||
return new PersistedGrant
|
||||
{
|
||||
Key = grant.Key,
|
||||
Type = grant.Type,
|
||||
SubjectId = grant.SubjectId,
|
||||
ClientId = grant.ClientId,
|
||||
CreationTime = grant.CreationDate,
|
||||
Expiration = grant.ExpirationDate,
|
||||
Data = grant.Data
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
34
src/Api/IdentityServer/ProfileService.cs
Normal file
34
src/Api/IdentityServer/ProfileService.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using IdentityServer4.Services;
|
||||
using System.Threading.Tasks;
|
||||
using IdentityServer4.Models;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Api.IdentityServer
|
||||
{
|
||||
public class ProfileService : IProfileService
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public ProfileService(
|
||||
IUserRepository userRepository,
|
||||
IUserService userService)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
public Task GetProfileDataAsync(ProfileDataRequestContext context)
|
||||
{
|
||||
context.AddFilteredClaims(context.Subject.Claims);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task IsActiveAsync(IsActiveContext context)
|
||||
{
|
||||
context.IsActive = true;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
118
src/Api/IdentityServer/ResourceOwnerPasswordValidator.cs
Normal file
118
src/Api/IdentityServer/ResourceOwnerPasswordValidator.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using Bit.Core.Domains;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Api.IdentityServer
|
||||
{
|
||||
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
|
||||
{
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly IdentityOptions _identityOptions;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
|
||||
public ResourceOwnerPasswordValidator(
|
||||
UserManager<User> userManager,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IDeviceRepository deviceRepository)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_identityOptions = optionsAccessor?.Value ?? new IdentityOptions();
|
||||
_deviceRepository = deviceRepository;
|
||||
}
|
||||
|
||||
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
|
||||
{
|
||||
var twoFactorCode = context.Request.Raw["twoFactorCode"]?.ToString();
|
||||
var twoFactorProvider = context.Request.Raw["twoFactorProvider"]?.ToString();
|
||||
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorCode) &&
|
||||
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(context.UserName.ToLowerInvariant());
|
||||
if(user != null)
|
||||
{
|
||||
if(await _userManager.CheckPasswordAsync(user, context.Password))
|
||||
{
|
||||
if(!twoFactorRequest && await TwoFactorRequiredAsync(user))
|
||||
{
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Two factor code required.",
|
||||
// TODO: return something better?
|
||||
new System.Collections.Generic.Dictionary<string, object> { { "TwoFactorRequired", true } });
|
||||
return;
|
||||
}
|
||||
|
||||
if(!twoFactorRequest || await _userManager.VerifyTwoFactorTokenAsync(user, twoFactorProvider, twoFactorCode))
|
||||
{
|
||||
await SaveDeviceAsync(user, context);
|
||||
|
||||
context.Result = new GrantValidationResult(user.Id.ToString(), "Application", identityProvider: "bitwarden",
|
||||
claims: new Claim[] {
|
||||
// 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.ToString()),
|
||||
new Claim(_identityOptions.ClaimsIdentity.SecurityStampClaimType, user.SecurityStamp)
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant,
|
||||
twoFactorRequest ? "Code is not correct. Try again." : "Username or password is incorrect. Try again.",
|
||||
new System.Collections.Generic.Dictionary<string, object> { { "Error", true } });
|
||||
}
|
||||
|
||||
private async Task<bool> TwoFactorRequiredAsync(User user)
|
||||
{
|
||||
return _userManager.SupportsUserTwoFactor &&
|
||||
await _userManager.GetTwoFactorEnabledAsync(user) &&
|
||||
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
|
||||
}
|
||||
|
||||
private Device GetDeviceFromRequest(ResourceOwnerPasswordValidationContext context)
|
||||
{
|
||||
var deviceIdentifier = context.Request.Raw["deviceIdentifier"]?.ToString();
|
||||
var deviceType = context.Request.Raw["deviceType"]?.ToString();
|
||||
var deviceName = context.Request.Raw["deviceName"]?.ToString();
|
||||
var devicePushToken = context.Request.Raw["devicePushToken"]?.ToString();
|
||||
|
||||
DeviceType type;
|
||||
if(string.IsNullOrWhiteSpace(deviceIdentifier) || string.IsNullOrWhiteSpace(deviceType) ||
|
||||
string.IsNullOrWhiteSpace(deviceName) || !Enum.TryParse(deviceType, out type))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Device
|
||||
{
|
||||
Identifier = deviceIdentifier,
|
||||
Name = deviceName,
|
||||
Type = type,
|
||||
PushToken = devicePushToken
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SaveDeviceAsync(User user, ResourceOwnerPasswordValidationContext context)
|
||||
{
|
||||
var device = GetDeviceFromRequest(context);
|
||||
if(device != null)
|
||||
{
|
||||
var existingDevice = await _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id);
|
||||
if(existingDevice == null)
|
||||
{
|
||||
device.UserId = user.Id;
|
||||
await _deviceRepository.CreateAsync(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30
src/Api/IdentityServer/TokenRetrieval.cs
Normal file
30
src/Api/IdentityServer/TokenRetrieval.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Bit.Api.IdentityServer
|
||||
{
|
||||
public static class TokenRetrieval
|
||||
{
|
||||
public static Func<HttpRequest, string> FromAuthorizationHeaderOrQueryString(string headerScheme = "Bearer",
|
||||
string qsName = "account_token")
|
||||
{
|
||||
return (request) =>
|
||||
{
|
||||
string authorization = request.Headers["Authorization"].FirstOrDefault();
|
||||
|
||||
if(string.IsNullOrWhiteSpace(authorization))
|
||||
{
|
||||
return request.Query[qsName].FirstOrDefault();
|
||||
}
|
||||
|
||||
if(authorization.StartsWith(headerScheme + " ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return authorization.Substring(headerScheme.Length + 1).Trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ using IdentityServer4.Stores;
|
||||
using Bit.Core.Utilities;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Bit.Api.IdentityServer;
|
||||
|
||||
namespace Bit.Api
|
||||
{
|
||||
|
@ -22,7 +22,9 @@
|
||||
"Microsoft.ApplicationInsights.AspNetCore": "1.0.2",
|
||||
"AspNetCoreRateLimit": "1.0.5",
|
||||
"Serilog.Extensions.Logging": "1.3.1",
|
||||
"Serilog.Sinks.AzureDocumentDb": "3.5.17"
|
||||
"Serilog.Sinks.AzureDocumentDb": "3.5.17",
|
||||
"IdentityServer4": "1.0.1",
|
||||
"IdentityServer4.AccessTokenValidation": "1.0.2"
|
||||
},
|
||||
|
||||
"tools": {
|
||||
|
Reference in New Issue
Block a user