diff --git a/src/Api/Controllers/AliveController.cs b/src/Api/Controllers/AliveController.cs index 17914bd89..32936e658 100644 --- a/src/Api/Controllers/AliveController.cs +++ b/src/Api/Controllers/AliveController.cs @@ -1,5 +1,6 @@ using System; using Microsoft.AspNetCore.Mvc; +using System.Linq; namespace Bit.Api.Controllers { @@ -11,5 +12,11 @@ namespace Bit.Api.Controllers { return DateTime.UtcNow; } + + [HttpGet("claims")] + public IActionResult Claims() + { + return new JsonResult(User.Claims.Select(c => new { c.Type, c.Value })); + } } } diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index c81949554..40e4d5ece 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -25,6 +25,10 @@ using Microsoft.Net.Http.Headers; using Newtonsoft.Json.Serialization; using AspNetCoreRateLimit; using Bit.Api.Middleware; +using IdentityServer4.Validation; +using IdentityServer4.Services; +using IdentityModel.AspNetCore.OAuth2Introspection; +using Microsoft.AspNetCore.Authorization.Infrastructure; namespace Bit.Api { @@ -81,6 +85,15 @@ namespace Bit.Api services.AddSingleton(); services.AddSingleton(); + // IdentityServer + services.AddIdentityServer() + // TODO: Add proper signing creds + .AddTemporarySigningCredential() + .AddInMemoryApiResources(Resources.GetApiResources()) + .AddInMemoryClients(Clients.GetClients()); + services.AddSingleton(); + services.AddSingleton(); + // Identity services.AddTransient(); services.AddJwtBearerIdentity(options => @@ -121,13 +134,19 @@ namespace Bit.Api var jwtIdentityOptions = provider.GetRequiredService>().Value; services.AddAuthorization(config => { - config.AddPolicy("Application", new AuthorizationPolicyBuilder() - .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​) - .RequireAuthenticatedUser().RequireClaim(ClaimTypes.AuthenticationMethod, jwtIdentityOptions.AuthenticationMethod).Build()); + config.AddPolicy("Application", policy => + { + policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme, "Bearer2"); + policy.RequireAuthenticatedUser(); + policy.RequireClaim(ClaimTypes.AuthenticationMethod, jwtIdentityOptions.AuthenticationMethod); + }); - config.AddPolicy("TwoFactor", new AuthorizationPolicyBuilder() - .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) - .RequireAuthenticatedUser().RequireClaim(ClaimTypes.AuthenticationMethod, jwtIdentityOptions.TwoFactorAuthenticationMethod).Build()); + config.AddPolicy("TwoFactor", policy => + { + policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme, "Bearer2"); + policy.RequireAuthenticatedUser(); + policy.RequireClaim(ClaimTypes.AuthenticationMethod, jwtIdentityOptions.TwoFactorAuthenticationMethod); + }); }); services.AddScoped(); @@ -207,6 +226,18 @@ namespace Bit.Api // Add Cors app.UseCors("All"); + // Add IdentityServer to the request pipeline. + app.UseIdentityServer(); + app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions + { + AllowedScopes = new string[] { "api" }, + Authority = env.IsProduction() ? "https://api.bitwarden.com" : "http://localhost:4000", + RequireHttpsMetadata = env.IsProduction(), + ApiName = "Vault API", + AuthenticationScheme = "Bearer2", + TokenRetriever = TokenRetrieval.FromAuthorizationHeader("Bearer2") + }); + // Add Jwt authentication to the request pipeline. app.UseJwtBearerIdentity(); diff --git a/src/Core/Identity/Clients.cs b/src/Core/Identity/Clients.cs new file mode 100644 index 000000000..8669d4f49 --- /dev/null +++ b/src/Core/Identity/Clients.cs @@ -0,0 +1,31 @@ +using IdentityServer4.Models; +using System.Collections.Generic; + +namespace Bit.Core.Identity +{ + public class Clients + { + public static IEnumerable GetClients() + { + return new List + { + 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" }; + } + } + } +} diff --git a/src/Core/Identity/ProfileService.cs b/src/Core/Identity/ProfileService.cs new file mode 100644 index 000000000..600fa1420 --- /dev/null +++ b/src/Core/Identity/ProfileService.cs @@ -0,0 +1,36 @@ +using IdentityServer4.Services; +using System.Threading.Tasks; +using IdentityServer4.Models; +using Bit.Core.Repositories; +using Bit.Core.Services; +using System.Security.Claims; + +namespace Bit.Core.Identity +{ + 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) + { + // TODO: load proper claims for user + context.AddFilteredClaims(new Claim[] { new Claim(ClaimTypes.AuthenticationMethod, "Application") }); + return Task.FromResult(0); + } + + public Task IsActiveAsync(IsActiveContext context) + { + context.IsActive = true; + return Task.FromResult(0); + } + } +} diff --git a/src/Core/Identity/ResourceOwnerPasswordValidator.cs b/src/Core/Identity/ResourceOwnerPasswordValidator.cs new file mode 100644 index 000000000..e121cb5d8 --- /dev/null +++ b/src/Core/Identity/ResourceOwnerPasswordValidator.cs @@ -0,0 +1,41 @@ +using Bit.Core.Domains; +using Bit.Core.Repositories; +using IdentityServer4.Models; +using IdentityServer4.Validation; +using Microsoft.AspNetCore.Identity; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Bit.Core.Identity +{ + public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator + { + private readonly IUserRepository _userRepository; + private readonly UserManager _userManager; + + public ResourceOwnerPasswordValidator( + IUserRepository userRepository, + UserManager userManager) + { + _userRepository = userRepository; + _userManager = userManager; + } + + public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) + { + var user = await _userRepository.GetByEmailAsync(context.UserName.ToLowerInvariant()); + if(user != null) + { + if(await _userManager.CheckPasswordAsync(user, context.Password)) + { + // TODO: proper claims and auth method + context.Result = new GrantValidationResult(subject: user.Id.ToString(), authenticationMethod: "Application", + identityProvider: "bitwarden", claims: new Claim[] { new Claim(ClaimTypes.AuthenticationMethod, "Application") }); + return; + } + } + + context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Username or password is incorrect."); + } + } +} diff --git a/src/Core/Identity/Resources.cs b/src/Core/Identity/Resources.cs new file mode 100644 index 000000000..0ed65914b --- /dev/null +++ b/src/Core/Identity/Resources.cs @@ -0,0 +1,17 @@ +using IdentityServer4.Models; +using System.Collections.Generic; +using System.Security.Claims; + +namespace Bit.Core.Identity +{ + public class Resources + { + public static IEnumerable GetApiResources() + { + return new List + { + new ApiResource("api", "Vault API", new string[] { ClaimTypes.AuthenticationMethod }) + }; + } + } +} diff --git a/src/Core/project.json b/src/Core/project.json index 1cd5c35f0..7d6c38e7e 100644 --- a/src/Core/project.json +++ b/src/Core/project.json @@ -8,7 +8,9 @@ "DataTableProxy": "1.2.0", "Sendgrid": "6.3.4", "PushSharp": "4.0.10", - "WindowsAzure.Storage": "8.0.0" + "WindowsAzure.Storage": "8.0.0", + "IdentityServer4": "1.0.1", + "IdentityServer4.AccessTokenValidation": "1.0.2" }, "frameworks": {