From 0f37920de2ce30b88493d03e44a55ac385559237 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 11 Aug 2017 08:57:31 -0400 Subject: [PATCH] push registration through relay apis --- docker/global.env | 2 + src/Api/Controllers/PushController.cs | 70 ++++- src/Core/GlobalSettings.cs | 13 +- .../Request/PushRegistrationRequestModel.cs | 19 ++ .../Api/Request/PushUpdateRequestModel.cs | 22 ++ src/Core/Services/IPushRegistrationService.cs | 13 +- .../Services/Implementations/DeviceService.cs | 7 +- .../NotificationHubPushRegistrationService.cs | 76 +++--- .../Implementations/OrganizationService.cs | 24 +- .../RelayPushRegistrationService.cs | 253 ++++++++++++++++++ .../NoopPushRegistrationService.cs | 13 +- util/Setup/Program.cs | 20 +- 12 files changed, 463 insertions(+), 69 deletions(-) create mode 100644 src/Core/Models/Api/Request/PushRegistrationRequestModel.cs create mode 100644 src/Core/Models/Api/Request/PushUpdateRequestModel.cs create mode 100644 src/Core/Services/Implementations/RelayPushRegistrationService.cs diff --git a/docker/global.env b/docker/global.env index 871ee20b2..d990f0bb4 100644 --- a/docker/global.env +++ b/docker/global.env @@ -4,3 +4,5 @@ globalSettings:baseServiceUri:vault=http://localhost globalSettings:baseServiceUri:api=http://localhost/api globalSettings:baseServiceUri:identity=http://localhost/identity globalSettings:baseServiceUri:internalIdentity=http://identity +globalSettings:pushRelayBaseUri=https://push.bitwarden.com +globalSettings:installation:identityUri=https://identity.bitwarden.com diff --git a/src/Api/Controllers/PushController.cs b/src/Api/Controllers/PushController.cs index 155350001..d6b843bd2 100644 --- a/src/Api/Controllers/PushController.cs +++ b/src/Api/Controllers/PushController.cs @@ -1,9 +1,12 @@ -using System; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Bit.Core; using Bit.Core.Exceptions; +using Bit.Core.Models.Api; +using System.Threading.Tasks; +using System.Linq; +using Microsoft.AspNetCore.Hosting; namespace Bit.Api.Controllers { @@ -12,25 +15,76 @@ namespace Bit.Api.Controllers public class PushController : Controller { private readonly IPushRegistrationService _pushRegistrationService; + private readonly IHostingEnvironment _environment; private readonly CurrentContext _currentContext; + private readonly GlobalSettings _globalSettings; public PushController( IPushRegistrationService pushRegistrationService, - CurrentContext currentContext) + IHostingEnvironment environment, + CurrentContext currentContext, + GlobalSettings globalSettings) { _currentContext = currentContext; + _environment = environment; _pushRegistrationService = pushRegistrationService; + _globalSettings = globalSettings; } - [HttpGet("register")] - public Object Register() + [HttpPost("register")] + public async Task PostRegister(PushRegistrationRequestModel model) { - if(!_currentContext.InstallationId.HasValue) + CheckUsage(); + await _pushRegistrationService.CreateOrUpdateRegistrationAsync(model.PushToken, Prefix(model.DeviceId), + Prefix(model.UserId), Prefix(model.Identifier), model.Type); + } + + [HttpDelete("{id}")] + public async Task Delete(string id) + { + CheckUsage(); + await _pushRegistrationService.DeleteRegistrationAsync(Prefix(id)); + } + + [HttpPut("add-organization")] + public async Task PutAddOrganization(PushUpdateRequestModel model) + { + CheckUsage(); + await _pushRegistrationService.AddUserRegistrationOrganizationAsync( + model.DeviceIds.Select(d => Prefix(d)), Prefix(model.OrganizationId)); + } + + [HttpPut("delete-organization")] + public async Task PutDeleteOrganization(PushUpdateRequestModel model) + { + CheckUsage(); + await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync( + model.DeviceIds.Select(d => Prefix(d)), Prefix(model.OrganizationId)); + } + + private string Prefix(string value) + { + return $"{_currentContext.InstallationId.Value}_{value}"; + } + + private void CheckUsage() + { + if(CanUse()) { - throw new BadRequestException("bad request."); + return; } - return new { Foo = "bar" }; + throw new BadRequestException("Not correctly configured for push relays."); + } + + private bool CanUse() + { + if(_environment.IsDevelopment()) + { + return true; + } + + return _currentContext.InstallationId.HasValue && _globalSettings.SelfHosted; } } } diff --git a/src/Core/GlobalSettings.cs b/src/Core/GlobalSettings.cs index b0ef548ab..c6ee88aca 100644 --- a/src/Core/GlobalSettings.cs +++ b/src/Core/GlobalSettings.cs @@ -1,4 +1,6 @@ -namespace Bit.Core +using System; + +namespace Bit.Core { public class GlobalSettings { @@ -8,6 +10,8 @@ public virtual string ProjectName { get; set; } public virtual string LogDirectory { get; set; } public virtual string LicenseDirectory { get; set; } + public virtual string PushRelayBaseUri { get; set; } + public virtual InstallationSettings Installation { get; set; } = new InstallationSettings(); public virtual BaseServiceUriSettings BaseServiceUri { get; set; } = new BaseServiceUriSettings(); public virtual SqlServerSettings SqlServer { get; set; } = new SqlServerSettings(); public virtual MailSettings Mail { get; set; } = new MailSettings(); @@ -104,5 +108,12 @@ public string PublicKey { get; set; } public string PrivateKey { get; set; } } + + public class InstallationSettings + { + public Guid? Id { get; set; } + public string Key { get; set; } + public string IdentityUri { get; set; } + } } } diff --git a/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs new file mode 100644 index 000000000..b628edc29 --- /dev/null +++ b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs @@ -0,0 +1,19 @@ +using Bit.Core.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class PushRegistrationRequestModel + { + [Required] + public string DeviceId { get; set; } + [Required] + public string PushToken { get; set; } + [Required] + public string UserId { get; set; } + [Required] + public DeviceType Type { get; set; } + [Required] + public string Identifier { get; set; } + } +} diff --git a/src/Core/Models/Api/Request/PushUpdateRequestModel.cs b/src/Core/Models/Api/Request/PushUpdateRequestModel.cs new file mode 100644 index 000000000..dc71a5d10 --- /dev/null +++ b/src/Core/Models/Api/Request/PushUpdateRequestModel.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class PushUpdateRequestModel + { + public PushUpdateRequestModel() + { } + + public PushUpdateRequestModel(IEnumerable deviceIds, string organizationId) + { + DeviceIds = deviceIds; + OrganizationId = organizationId; + } + + [Required] + public IEnumerable DeviceIds { get; set; } + [Required] + public string OrganizationId { get; set; } + } +} diff --git a/src/Core/Services/IPushRegistrationService.cs b/src/Core/Services/IPushRegistrationService.cs index 38282bccf..20d79b1c5 100644 --- a/src/Core/Services/IPushRegistrationService.cs +++ b/src/Core/Services/IPushRegistrationService.cs @@ -1,14 +1,15 @@ -using System; +using System.Collections.Generic; using System.Threading.Tasks; -using Bit.Core.Models.Table; +using Bit.Core.Enums; namespace Bit.Core.Services { public interface IPushRegistrationService { - Task CreateOrUpdateRegistrationAsync(Device device); - Task DeleteRegistrationAsync(Guid deviceId); - Task AddUserRegistrationOrganizationAsync(Guid userId, Guid organizationId); - Task DeleteUserRegistrationOrganizationAsync(Guid userId, Guid organizationId); + Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, + string identifier, DeviceType type); + Task DeleteRegistrationAsync(string deviceId); + Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); + Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); } } diff --git a/src/Core/Services/Implementations/DeviceService.cs b/src/Core/Services/Implementations/DeviceService.cs index 236d946e6..ca93af8a7 100644 --- a/src/Core/Services/Implementations/DeviceService.cs +++ b/src/Core/Services/Implementations/DeviceService.cs @@ -30,19 +30,20 @@ namespace Bit.Core.Services await _deviceRepository.ReplaceAsync(device); } - await _pushRegistrationService.CreateOrUpdateRegistrationAsync(device); + await _pushRegistrationService.CreateOrUpdateRegistrationAsync(device.PushToken, device.Id.ToString(), + device.UserId.ToString(), device.Identifier, device.Type); } public async Task ClearTokenAsync(Device device) { await _deviceRepository.ClearPushTokenAsync(device.Id); - await _pushRegistrationService.DeleteRegistrationAsync(device.Id); + await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString()); } public async Task DeleteAsync(Device device) { await _deviceRepository.DeleteAsync(device); - await _pushRegistrationService.DeleteRegistrationAsync(device.Id); + await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString()); } } } diff --git a/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs b/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs index ed0fde9eb..24c870b03 100644 --- a/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs +++ b/src/Core/Services/Implementations/NotificationHubPushRegistrationService.cs @@ -1,63 +1,59 @@ #if NET461 -using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Azure.NotificationHubs; -using Bit.Core.Models.Table; -using Bit.Core.Repositories; +using Bit.Core.Enums; +using System.Linq; namespace Bit.Core.Services { public class NotificationHubPushRegistrationService : IPushRegistrationService { private readonly NotificationHubClient _client; - private readonly IDeviceRepository _deviceRepository; public NotificationHubPushRegistrationService( - GlobalSettings globalSettings, - IDeviceRepository deviceRepository) + GlobalSettings globalSettings) { _client = NotificationHubClient.CreateClientFromConnectionString(globalSettings.NotificationHub.ConnectionString, globalSettings.NotificationHub.HubName); - - _deviceRepository = deviceRepository; } - public async Task CreateOrUpdateRegistrationAsync(Device device) + public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, + string identifier, DeviceType type) { - if(string.IsNullOrWhiteSpace(device.PushToken)) + if(string.IsNullOrWhiteSpace(pushToken)) { return; } - var installation = new Microsoft.Azure.NotificationHubs.Installation + var installation = new Installation { - InstallationId = device.Id.ToString(), - PushChannel = device.PushToken, + InstallationId = deviceId, + PushChannel = pushToken, Templates = new Dictionary() }; installation.Tags = new List { - $"userId:{device.UserId}" + $"userId:{userId}" }; - if(!string.IsNullOrWhiteSpace(device.Identifier)) + if(!string.IsNullOrWhiteSpace(identifier)) { - installation.Tags.Add("deviceIdentifier:" + device.Identifier); + installation.Tags.Add("deviceIdentifier:" + identifier); } string payloadTemplate = null, messageTemplate = null, badgeMessageTemplate = null; - switch(device.Type) + switch(type) { - case Enums.DeviceType.Android: + case DeviceType.Android: payloadTemplate = "{\"data\":{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}}"; messageTemplate = "{\"data\":{\"data\":{\"type\":\"#(type)\"}," + "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}"; installation.Platform = NotificationPlatform.Gcm; break; - case Enums.DeviceType.iOS: + case DeviceType.iOS: payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}," + "\"aps\":{\"alert\":null,\"badge\":null,\"content-available\":1}}"; messageTemplate = "{\"data\":{\"type\":\"#(type)\"}," + @@ -67,7 +63,7 @@ namespace Bit.Core.Services installation.Platform = NotificationPlatform.Apns; break; - case Enums.DeviceType.AndroidAmazon: + case DeviceType.AndroidAmazon: payloadTemplate = "{\"data\":{\"type\":\"#(type)\",\"payload\":\"$(payload)\"}}"; messageTemplate = "{\"data\":{\"type\":\"#(type)\",\"message\":\"$(message)\"}}"; @@ -77,16 +73,15 @@ namespace Bit.Core.Services break; } - BuildInstallationTemplate(installation, "payload", payloadTemplate, device.UserId, device.Identifier); - BuildInstallationTemplate(installation, "message", messageTemplate, device.UserId, device.Identifier); - BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate, device.UserId, - device.Identifier); + BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier); + BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier); + BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate, userId, identifier); await _client.CreateOrUpdateInstallationAsync(installation); } - private void BuildInstallationTemplate(Microsoft.Azure.NotificationHubs.Installation installation, - string templateId, string templateBody, Guid userId, string deviceIdentifier) + private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody, + string userId, string identifier) { if(templateBody == null) { @@ -105,32 +100,37 @@ namespace Bit.Core.Services } }; - if(!string.IsNullOrWhiteSpace(deviceIdentifier)) + if(!string.IsNullOrWhiteSpace(identifier)) { - template.Tags.Add($"{fullTemplateId}_deviceIdentifier:{deviceIdentifier}"); + template.Tags.Add($"{fullTemplateId}_deviceIdentifier:{identifier}"); } installation.Templates.Add(fullTemplateId, template); } - public async Task DeleteRegistrationAsync(Guid deviceId) + public async Task DeleteRegistrationAsync(string deviceId) { - await _client.DeleteInstallationAsync(deviceId.ToString()); + await _client.DeleteInstallationAsync(deviceId); } - public async Task AddUserRegistrationOrganizationAsync(Guid userId, Guid organizationId) + public async Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) { - await PatchTagsForUserDevicesAsync(userId, UpdateOperationType.Add, $"organizationId:{organizationId}"); + await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Add, $"organizationId:{organizationId}"); } - public async Task DeleteUserRegistrationOrganizationAsync(Guid userId, Guid organizationId) + public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) { - await PatchTagsForUserDevicesAsync(userId, UpdateOperationType.Remove, $"organizationId:{organizationId}"); + await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Remove, + $"organizationId:{organizationId}"); } - private async Task PatchTagsForUserDevicesAsync(Guid userId, UpdateOperationType op, string tag) + private async Task PatchTagsForUserDevicesAsync(IEnumerable deviceIds, UpdateOperationType op, string tag) { - var devices = await _deviceRepository.GetManyByUserIdAsync(userId); + if(!deviceIds.Any()) + { + return; + } + var operation = new PartialUpdateOperation { Operation = op, @@ -138,9 +138,9 @@ namespace Bit.Core.Services Value = tag }; - foreach(var device in devices) + foreach(var id in deviceIds) { - await _client.PatchInstallationAsync(device.Id.ToString(), new List { operation }); + await _client.PatchInstallationAsync(id, new List { operation }); } } } diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 6b466742c..21c763dbf 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -25,6 +25,7 @@ namespace Bit.Core.Services private readonly IMailService _mailService; private readonly IPushNotificationService _pushNotificationService; private readonly IPushRegistrationService _pushRegistrationService; + private readonly IDeviceRepository _deviceRepository; private readonly StripePaymentService _stripePaymentService; public OrganizationService( @@ -36,7 +37,8 @@ namespace Bit.Core.Services IDataProtectionProvider dataProtectionProvider, IMailService mailService, IPushNotificationService pushNotificationService, - IPushRegistrationService pushRegistrationService) + IPushRegistrationService pushRegistrationService, + IDeviceRepository deviceRepository) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -47,6 +49,7 @@ namespace Bit.Core.Services _mailService = mailService; _pushNotificationService = pushNotificationService; _pushRegistrationService = pushRegistrationService; + _deviceRepository = deviceRepository; _stripePaymentService = new StripePaymentService(); } @@ -496,8 +499,8 @@ namespace Bit.Core.Services await _organizationUserRepository.CreateAsync(orgUser); // push - await _pushRegistrationService.AddUserRegistrationOrganizationAsync(orgUser.UserId.Value, - organization.Id); + var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value); + await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds, organization.Id.ToString()); await _pushNotificationService.PushSyncOrgKeysAsync(signup.Owner.Id); return new Tuple(organization, orgUser); @@ -732,7 +735,8 @@ namespace Bit.Core.Services } // push - await _pushRegistrationService.AddUserRegistrationOrganizationAsync(orgUser.UserId.Value, organizationId); + var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value); + await _pushRegistrationService.AddUserRegistrationOrganizationAsync(deviceIds, organizationId.ToString()); await _pushNotificationService.PushSyncOrgKeysAsync(orgUser.UserId.Value); return orgUser; @@ -784,7 +788,8 @@ namespace Bit.Core.Services if(orgUser.UserId.HasValue) { // push - await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(orgUser.UserId.Value, organizationId); + var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value); + await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds, organizationId.ToString()); } } @@ -807,7 +812,8 @@ namespace Bit.Core.Services if(orgUser.UserId.HasValue) { // push - await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(orgUser.UserId.Value, organizationId); + var deviceIds = await GetUserDeviceIdsAsync(orgUser.UserId.Value); + await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(deviceIds, organizationId.ToString()); } } @@ -980,5 +986,11 @@ namespace Bit.Core.Services OrganizationUserType.Owner); return owners.Where(o => o.Status == OrganizationUserStatusType.Confirmed); } + + private async Task> GetUserDeviceIdsAsync(Guid userId) + { + var devices = await _deviceRepository.GetManyByUserIdAsync(userId); + return devices.Select(d => d.Id.ToString()); + } } } diff --git a/src/Core/Services/Implementations/RelayPushRegistrationService.cs b/src/Core/Services/Implementations/RelayPushRegistrationService.cs new file mode 100644 index 000000000..cc15f69a1 --- /dev/null +++ b/src/Core/Services/Implementations/RelayPushRegistrationService.cs @@ -0,0 +1,253 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Net.Http; +using Newtonsoft.Json; +using System.Text; +using System; +using Newtonsoft.Json.Linq; +using Bit.Core.Utilities; +using System.Net; +using Bit.Core.Models.Api; +using Bit.Core.Enums; +using System.Linq; + +namespace Bit.Core.Services +{ + public class RelayPushRegistrationService : IPushRegistrationService + { + private readonly HttpClient _pushClient; + private readonly HttpClient _identityClient; + private readonly GlobalSettings _globalSettings; + private string _accessToken; + private dynamic _decodedToken; + private DateTime? _nextAuthAttempt = null; + + public RelayPushRegistrationService( + GlobalSettings globalSettings) + { + _globalSettings = globalSettings; + + _pushClient = new HttpClient + { + BaseAddress = new Uri(globalSettings.PushRelayBaseUri) + }; + + _identityClient = new HttpClient + { + BaseAddress = new Uri(globalSettings.Installation.IdentityUri) + }; + } + + public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, + string identifier, DeviceType type) + { + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse) + { + return; + } + + var requestModel = new PushRegistrationRequestModel + { + DeviceId = deviceId, + Identifier = identifier, + PushToken = pushToken, + Type = type, + UserId = userId + }; + + var message = new TokenHttpRequestMessage(requestModel, _accessToken) + { + Method = HttpMethod.Post, + RequestUri = new Uri(_pushClient.BaseAddress, "register") + }; + await _pushClient.SendAsync(message); + } + + public async Task DeleteRegistrationAsync(string deviceId) + { + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse) + { + return; + } + + var message = new TokenHttpRequestMessage(_accessToken) + { + Method = HttpMethod.Delete, + RequestUri = new Uri(_pushClient.BaseAddress, deviceId) + }; + await _pushClient.SendAsync(message); + } + + public async Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) + { + if(!deviceIds.Any()) + { + return; + } + + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse) + { + return; + } + + var requestModel = new PushUpdateRequestModel(deviceIds, organizationId); + var message = new TokenHttpRequestMessage(requestModel, _accessToken) + { + Method = HttpMethod.Put, + RequestUri = new Uri(_pushClient.BaseAddress, "add-organization") + }; + await _pushClient.SendAsync(message); + } + + public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) + { + if(!deviceIds.Any()) + { + return; + } + + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse) + { + return; + } + + var requestModel = new PushUpdateRequestModel(deviceIds, organizationId); + var message = new TokenHttpRequestMessage(requestModel, _accessToken) + { + Method = HttpMethod.Put, + RequestUri = new Uri(_pushClient.BaseAddress, "delete-organization") + }; + await _pushClient.SendAsync(message); + } + + private async Task HandleTokenStateAsync() + { + if(_nextAuthAttempt.HasValue && DateTime.UtcNow > _nextAuthAttempt.Value) + { + return false; + } + _nextAuthAttempt = null; + + if(!string.IsNullOrWhiteSpace(_accessToken) && !TokenExpired()) + { + return true; + } + + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri(_identityClient.BaseAddress, "connect/token"), + Content = new FormUrlEncodedContent(new Dictionary + { + { "grant_type", "client_credentials" }, + { "scope", "api.push" }, + { "client_id", $"installation.{_globalSettings.Installation.Id}" }, + { "client_secret", $"{_globalSettings.Installation.Key}" } + }) + }; + + var response = await _identityClient.SendAsync(requestMessage); + if(!response.IsSuccessStatusCode) + { + if(response.StatusCode == HttpStatusCode.BadRequest) + { + _nextAuthAttempt = DateTime.UtcNow.AddDays(1); + } + + return false; + } + + var responseContent = await response.Content.ReadAsStringAsync(); + dynamic tokenResponse = JsonConvert.DeserializeObject(responseContent); + _accessToken = (string)tokenResponse.access_token; + return true; + } + + public class TokenHttpRequestMessage : HttpRequestMessage + { + public TokenHttpRequestMessage(string token) + { + Headers.Add("Authorization", $"Bearer {token}"); + } + + public TokenHttpRequestMessage(object requestObject, string token) + : this(token) + { + var stringContent = JsonConvert.SerializeObject(requestObject); + Content = new StringContent(stringContent, Encoding.UTF8, "application/json"); + } + } + + public bool TokenExpired() + { + var decoded = DecodeToken(); + var exp = decoded?["exp"]; + if(exp == null) + { + throw new InvalidOperationException("No exp in token."); + } + + var expiration = CoreHelpers.FromEpocMilliseconds(1000 * exp.Value()); + return DateTime.UtcNow < expiration; + } + + private JObject DecodeToken() + { + if(_decodedToken != null) + { + return _decodedToken; + } + + if(_accessToken == null) + { + throw new InvalidOperationException($"{nameof(_accessToken)} not found."); + } + + var parts = _accessToken.Split('.'); + if(parts.Length != 3) + { + throw new InvalidOperationException($"{nameof(_accessToken)} must have 3 parts"); + } + + var decodedBytes = Base64UrlDecode(parts[1]); + if(decodedBytes == null || decodedBytes.Length < 1) + { + throw new InvalidOperationException($"{nameof(_accessToken)} must have 3 parts"); + } + + _decodedToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes, 0, decodedBytes.Length)); + return _decodedToken; + } + + private byte[] Base64UrlDecode(string input) + { + var output = input; + // 62nd char of encoding + output = output.Replace('-', '+'); + // 63rd char of encoding + output = output.Replace('_', '/'); + // Pad with trailing '='s + switch(output.Length % 4) + { + case 0: + // No pad chars in this case + break; + case 2: + // Two pad chars + output += "=="; break; + case 3: + // One pad char + output += "="; break; + default: + throw new InvalidOperationException("Illegal base64url string!"); + } + + // Standard base64 decoder + return Convert.FromBase64String(output); + } + } +} diff --git a/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs b/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs index 79f85e8bc..506835e8a 100644 --- a/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs +++ b/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs @@ -1,27 +1,28 @@ -using System; +using System.Collections.Generic; using System.Threading.Tasks; -using Bit.Core.Models.Table; +using Bit.Core.Enums; namespace Bit.Core.Services { public class NoopPushRegistrationService : IPushRegistrationService { - public Task AddUserRegistrationOrganizationAsync(Guid userId, Guid organizationId) + public Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) { return Task.FromResult(0); } - public Task CreateOrUpdateRegistrationAsync(Device device) + public Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, + string identifier, DeviceType type) { return Task.FromResult(0); } - public Task DeleteRegistrationAsync(Guid deviceId) + public Task DeleteRegistrationAsync(string deviceId) { return Task.FromResult(0); } - public Task DeleteUserRegistrationOrganizationAsync(Guid userId, Guid organizationId) + public Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId) { return Task.FromResult(0); } diff --git a/util/Setup/Program.cs b/util/Setup/Program.cs index 2c4678e7a..2e358e674 100644 --- a/util/Setup/Program.cs +++ b/util/Setup/Program.cs @@ -17,6 +17,9 @@ namespace Setup private static bool _ssl = false; private static bool _selfSignedSsl = false; private static bool _letsEncrypt = false; + private static string _installationId = null; + private static string _installationKey = null; + private static bool _push = false; public static void Main(string[] args) { @@ -50,6 +53,14 @@ namespace Setup _url = _ssl ? $"https://{_domain}" : $"http://{_domain}"; BuildNginxConfig(); + + Console.Write("Installation ID: "); + _installationId = Console.ReadLine().ToLowerInvariant(); + Console.Write("Installation key: "); + _installationKey = Console.ReadLine().ToLowerInvariant(); + Console.Write("Do you want to use push notifications? (y/n): "); + _push = Console.ReadLine().ToLowerInvariant() == "y"; + BuildEnvironmentFiles(); BuildAppSettingsFiles(); } @@ -262,8 +273,15 @@ globalSettings:dataProtection:directory={_outputDir}/core/aspnet-dataprotection globalSettings:logDirectory={_outputDir}/core/logs globalSettings:licenseDirectory={_outputDir}/core/licenses globalSettings:duo:aKey={Helpers.SecureRandomString(32, alpha: true, numeric: true)} +globalSettings:installation:id={_installationId} +globalSettings:installation:key={_installationKey} globalSettings:yubico:clientId=REPLACE -globalSettings:yubico:REPLACE"); +globalSettings:yubico:key=REPLACE"); + + if(!_push) + { + sw.Write("globalSettings:pushRelayBaseUri=REPLACE"); + } } using(var sw = File.CreateText("/bitwarden/docker/mssql.override.env"))