diff --git a/src/Api/Controllers/PushController.cs b/src/Api/Controllers/PushController.cs new file mode 100644 index 0000000000..0cbc43d87c --- /dev/null +++ b/src/Api/Controllers/PushController.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Api.Controllers +{ + [Route("push")] + [Authorize("Push")] + public class PushController : Controller + { + private readonly IPushRegistrationService _pushRegistrationService; + + public PushController( + IPushRegistrationService pushRegistrationService) + { + _pushRegistrationService = pushRegistrationService; + } + + [HttpGet("register")] + public Object Register() + { + return new { Foo = "bar" }; + } + } +} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 67a30226bc..3adbc3e762 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -86,6 +86,11 @@ namespace Bit.Api policy.RequireClaim(JwtClaimTypes.Scope, "api"); policy.RequireClaim(JwtClaimTypes.ClientId, "web"); }); + config.AddPolicy("Push", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim(JwtClaimTypes.Scope, "api.push"); + }); }); services.AddScoped(); @@ -179,9 +184,8 @@ namespace Bit.Api var options = new IdentityServerAuthenticationOptions { Authority = globalSettings.BaseServiceUri.InternalIdentity, - AllowedScopes = new string[] { "api" }, + AllowedScopes = new string[] { "api", "api.push" }, RequireHttpsMetadata = !env.IsDevelopment() && globalSettings.BaseServiceUri.InternalIdentity.StartsWith("https"), - ApiName = "api", NameClaimType = ClaimTypes.Email, // Suffix until we retire the old jwt schemes. AuthenticationScheme = $"Bearer{suffix}", diff --git a/src/Core/IdentityServer/ApiResources.cs b/src/Core/IdentityServer/ApiResources.cs index 88d50d57a8..70da942a00 100644 --- a/src/Core/IdentityServer/ApiResources.cs +++ b/src/Core/IdentityServer/ApiResources.cs @@ -1,7 +1,6 @@ using IdentityModel; using IdentityServer4.Models; using System.Collections.Generic; -using System.Security.Claims; namespace Bit.Core.IdentityServer { @@ -21,7 +20,8 @@ namespace Bit.Core.IdentityServer "orgowner", "orgadmin", "orguser" - }) + }), + new ApiResource("api.push") }; } } diff --git a/src/Core/IdentityServer/ClientStore.cs b/src/Core/IdentityServer/ClientStore.cs new file mode 100644 index 0000000000..7dfc063e0e --- /dev/null +++ b/src/Core/IdentityServer/ClientStore.cs @@ -0,0 +1,49 @@ +using IdentityServer4.Stores; +using System.Threading.Tasks; +using IdentityServer4.Models; +using System.Collections.Generic; +using Bit.Core.Repositories; +using System; + +namespace Bit.Core.IdentityServer +{ + public class ClientStore : IClientStore + { + private static IDictionary _apiClients = StaticClients.GetApiClients(); + + private readonly IInstallationRepository _installationRepository; + public ClientStore( + IInstallationRepository installationRepository) + { + _installationRepository = installationRepository; + } + + public async Task FindClientByIdAsync(string clientId) + { + if(clientId.StartsWith("installation.")) + { + var idParts = clientId.Split('.'); + Guid id; + if(idParts.Length > 1 && Guid.TryParse(idParts[1], out id)) + { + var installation = await _installationRepository.GetByIdAsync(id); + if(installation != null) + { + return new Client + { + ClientId = $"installation.{installation.Id}", + RequireClientSecret = true, + ClientSecrets = { new Secret(installation.Key.Sha256()) }, + AllowedScopes = new string[] { "api.push" }, + AllowedGrantTypes = GrantTypes.ClientCredentials, + AccessTokenLifetime = 3600 * 24, + Enabled = installation.Enabled + }; + } + } + } + + return _apiClients.ContainsKey(clientId) ? _apiClients[clientId] : null; + } + } +} diff --git a/src/Core/IdentityServer/Clients.cs b/src/Core/IdentityServer/StaticClients.cs similarity index 90% rename from src/Core/IdentityServer/Clients.cs rename to src/Core/IdentityServer/StaticClients.cs index 47bf8162e9..61c2a2ce8b 100644 --- a/src/Core/IdentityServer/Clients.cs +++ b/src/Core/IdentityServer/StaticClients.cs @@ -1,11 +1,12 @@ using IdentityServer4.Models; using System.Collections.Generic; +using System.Linq; namespace Bit.Core.IdentityServer { - public class Clients + public class StaticClients { - public static IEnumerable GetClients() + public static IDictionary GetApiClients() { return new List { @@ -14,7 +15,7 @@ namespace Bit.Core.IdentityServer new ApiClient("browser", 30, 1), new ApiClient("desktop", 30, 1), new ApiClient("connector", 30, 24) - }; + }.ToDictionary(c => c.ClientId); } public class ApiClient : Client diff --git a/src/Core/Models/Table/Installation.cs b/src/Core/Models/Table/Installation.cs new file mode 100644 index 0000000000..e7abc7d0d7 --- /dev/null +++ b/src/Core/Models/Table/Installation.cs @@ -0,0 +1,19 @@ +using Bit.Core.Utilities; +using System; + +namespace Bit.Core.Models.Table +{ + public class Installation : ITableObject + { + public Guid Id { get; set; } + public string Email { get; set; } + public string Key { get; set; } + public bool Enabled { get; set; } + public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; + + public void SetNewId() + { + Id = CoreHelpers.GenerateComb(); + } + } +} diff --git a/src/Core/Repositories/IInstallationRepository.cs b/src/Core/Repositories/IInstallationRepository.cs new file mode 100644 index 0000000000..7c424521b1 --- /dev/null +++ b/src/Core/Repositories/IInstallationRepository.cs @@ -0,0 +1,9 @@ +using System; +using Bit.Core.Models.Table; + +namespace Bit.Core.Repositories +{ + public interface IInstallationRepository : IRepository + { + } +} diff --git a/src/Core/Repositories/SqlServer/InstallationRepository.cs b/src/Core/Repositories/SqlServer/InstallationRepository.cs new file mode 100644 index 0000000000..12d61c3ccc --- /dev/null +++ b/src/Core/Repositories/SqlServer/InstallationRepository.cs @@ -0,0 +1,16 @@ +using System; +using Bit.Core.Models.Table; + +namespace Bit.Core.Repositories.SqlServer +{ + public class InstallationRepository : Repository, IInstallationRepository + { + public InstallationRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString) + { } + + public InstallationRepository(string connectionString) + : base(connectionString) + { } + } +} diff --git a/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs b/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs index 81f249f268..ed0fde9ebd 100644 --- a/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs +++ b/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs @@ -30,7 +30,7 @@ namespace Bit.Core.Services return; } - var installation = new Installation + var installation = new Microsoft.Azure.NotificationHubs.Installation { InstallationId = device.Id.ToString(), PushChannel = device.PushToken, @@ -85,8 +85,8 @@ namespace Bit.Core.Services await _client.CreateOrUpdateInstallationAsync(installation); } - private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody, - Guid userId, string deviceIdentifier) + private void BuildInstallationTemplate(Microsoft.Azure.NotificationHubs.Installation installation, + string templateId, string templateBody, Guid userId, string deviceIdentifier) { if(templateBody == null) { diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index 571ee4d6bc..b1b09b2748 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -39,6 +39,7 @@ namespace Bit.Core.Utilities services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } public static void AddBaseServices(this IServiceCollection services) @@ -158,7 +159,7 @@ namespace Bit.Core.Utilities { SecurityStampClaimType = "sstamp", UserNameClaimType = JwtClaimTypes.Email, - UserIdClaimType = JwtClaimTypes.Subject, + UserIdClaimType = JwtClaimTypes.Subject }; options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider; }); @@ -190,11 +191,11 @@ namespace Bit.Core.Utilities options.Endpoints.EnableCheckSessionEndpoint = false; options.Endpoints.EnableTokenRevocationEndpoint = false; options.IssuerUri = globalSettings.BaseServiceUri.InternalIdentity; + options.Caching.ClientStoreExpiration = new TimeSpan(0, 5, 0); }) + .AddInMemoryCaching() .AddInMemoryApiResources(ApiResources.GetApiResources()) - .AddInMemoryClients(Clients.GetClients()); - - services.AddTransient(); + .AddClientStoreCache(); if(env.IsDevelopment()) { @@ -217,6 +218,8 @@ namespace Bit.Core.Utilities throw new Exception("No identity certificate to use."); } + services.AddTransient(); + services.AddTransient(); services.AddScoped(); services.AddScoped(); services.AddSingleton(); diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index fb5a8c04b1..e184c195c1 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -204,5 +204,11 @@ + + + + + + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Installation_Create.sql b/src/Sql/dbo/Stored Procedures/Installation_Create.sql new file mode 100644 index 0000000000..cc862c6474 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Installation_Create.sql @@ -0,0 +1,27 @@ +CREATE PROCEDURE [dbo].[Installation_Create] + @Id UNIQUEIDENTIFIER, + @Email NVARCHAR(50), + @Key VARCHAR(150), + @Enabled BIT, + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Installation] + ( + [Id], + [Email], + [Key], + [Enabled], + [CreationDate] + ) + VALUES + ( + @Id, + @Email, + @Key, + @Enabled, + @CreationDate + ) +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Installation_DeleteById.sql b/src/Sql/dbo/Stored Procedures/Installation_DeleteById.sql new file mode 100644 index 0000000000..9f7f2669d1 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Installation_DeleteById.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[Installation_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[Installation] + WHERE + [Id] = @Id +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Installation_ReadById.sql b/src/Sql/dbo/Stored Procedures/Installation_ReadById.sql new file mode 100644 index 0000000000..3e9b208bcb --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Installation_ReadById.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[Installation_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[InstallationView] + WHERE + [Id] = @Id +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Installation_Update.sql b/src/Sql/dbo/Stored Procedures/Installation_Update.sql new file mode 100644 index 0000000000..b56a351503 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Installation_Update.sql @@ -0,0 +1,20 @@ +CREATE PROCEDURE [dbo].[Installation_Update] + @Id UNIQUEIDENTIFIER, + @Email NVARCHAR(50), + @Key VARCHAR(150), + @Enabled BIT, + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Installation] + SET + [Email] = @Email, + [Key] = @Key, + [Enabled] = @Enabled, + [CreationDate] = @CreationDate + WHERE + [Id] = @Id +END \ No newline at end of file diff --git a/src/Sql/dbo/Tables/Installation.sql b/src/Sql/dbo/Tables/Installation.sql new file mode 100644 index 0000000000..17304e6239 --- /dev/null +++ b/src/Sql/dbo/Tables/Installation.sql @@ -0,0 +1,9 @@ +CREATE TABLE [dbo].[Installation] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [Email] NVARCHAR (50) NOT NULL, + [Key] VARCHAR (150) NOT NULL, + [Enabled] BIT NOT NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + CONSTRAINT [PK_Installation] PRIMARY KEY CLUSTERED ([Id] ASC) +); + diff --git a/src/Sql/dbo/Views/InstallationView.sql b/src/Sql/dbo/Views/InstallationView.sql new file mode 100644 index 0000000000..f230444cf1 --- /dev/null +++ b/src/Sql/dbo/Views/InstallationView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[InstallationView] +AS +SELECT + * +FROM + [dbo].[Installation] \ No newline at end of file