1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 16:12:49 -05:00

push registration through relay apis

This commit is contained in:
Kyle Spearrin
2017-08-11 08:57:31 -04:00
parent 0ad76a5487
commit 0f37920de2
12 changed files with 463 additions and 69 deletions

View File

@ -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());
}
}
}

View File

@ -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<string, InstallationTemplate>()
};
installation.Tags = new List<string>
{
$"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<string> 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<string> 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<string> 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<PartialUpdateOperation> { operation });
await _client.PatchInstallationAsync(id, new List<PartialUpdateOperation> { operation });
}
}
}

View File

@ -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, OrganizationUser>(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<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
{
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
return devices.Select(d => d.Id.ToString());
}
}
}

View File

@ -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<string> 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<string> 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<bool> 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<string, string>
{
{ "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<long>());
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);
}
}
}