mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 00:22:50 -05:00
push notification relay service and relay send api
This commit is contained in:
@ -1,42 +1,21 @@
|
||||
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
|
||||
public class RelayPushRegistrationService : BaseRelayPushNotificationService, 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 RelayPushRegistrationService(GlobalSettings globalSettings)
|
||||
: base(globalSettings)
|
||||
{ }
|
||||
|
||||
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||
string identifier, DeviceType type)
|
||||
@ -56,12 +35,12 @@ namespace Bit.Core.Services
|
||||
UserId = userId
|
||||
};
|
||||
|
||||
var message = new TokenHttpRequestMessage(requestModel, _accessToken)
|
||||
var message = new TokenHttpRequestMessage(requestModel, AccessToken)
|
||||
{
|
||||
Method = HttpMethod.Post,
|
||||
RequestUri = new Uri(_pushClient.BaseAddress, "register")
|
||||
RequestUri = new Uri(PushClient.BaseAddress, "register")
|
||||
};
|
||||
await _pushClient.SendAsync(message);
|
||||
await PushClient.SendAsync(message);
|
||||
}
|
||||
|
||||
public async Task DeleteRegistrationAsync(string deviceId)
|
||||
@ -72,12 +51,12 @@ namespace Bit.Core.Services
|
||||
return;
|
||||
}
|
||||
|
||||
var message = new TokenHttpRequestMessage(_accessToken)
|
||||
var message = new TokenHttpRequestMessage(AccessToken)
|
||||
{
|
||||
Method = HttpMethod.Delete,
|
||||
RequestUri = new Uri(_pushClient.BaseAddress, deviceId)
|
||||
RequestUri = new Uri(PushClient.BaseAddress, deviceId)
|
||||
};
|
||||
await _pushClient.SendAsync(message);
|
||||
await PushClient.SendAsync(message);
|
||||
}
|
||||
|
||||
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||
@ -94,12 +73,12 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||
var message = new TokenHttpRequestMessage(requestModel, _accessToken)
|
||||
var message = new TokenHttpRequestMessage(requestModel, AccessToken)
|
||||
{
|
||||
Method = HttpMethod.Put,
|
||||
RequestUri = new Uri(_pushClient.BaseAddress, "add-organization")
|
||||
RequestUri = new Uri(PushClient.BaseAddress, "add-organization")
|
||||
};
|
||||
await _pushClient.SendAsync(message);
|
||||
await PushClient.SendAsync(message);
|
||||
}
|
||||
|
||||
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||
@ -116,138 +95,12 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||
var message = new TokenHttpRequestMessage(requestModel, _accessToken)
|
||||
var message = new TokenHttpRequestMessage(requestModel, AccessToken)
|
||||
{
|
||||
Method = HttpMethod.Put,
|
||||
RequestUri = new Uri(_pushClient.BaseAddress, "delete-organization")
|
||||
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);
|
||||
await PushClient.SendAsync(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user