170 lines
6.2 KiB
C#
170 lines
6.2 KiB
C#
using Disco.Data.Repository;
|
|
using Newtonsoft.Json;
|
|
using System;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Disco.Services.Interop.DiscoServices
|
|
{
|
|
public static class OnlineServicesAuthentication
|
|
{
|
|
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
|
|
private static readonly Guid deploymentId;
|
|
private static Guid? activationId;
|
|
private static byte[] key;
|
|
private static string token;
|
|
private static DateTime? tokenExpires;
|
|
|
|
static OnlineServicesAuthentication()
|
|
{
|
|
using (var database = new DiscoDataContext())
|
|
{
|
|
deploymentId = Guid.Parse(database.DiscoConfiguration.DeploymentId);
|
|
UpdateActivation(database);
|
|
}
|
|
}
|
|
|
|
public static bool IsActivated => activationId.HasValue;
|
|
|
|
public static string GetToken()
|
|
=> GetTokenAsync().Result;
|
|
|
|
public async static Task<string> GetTokenAsync()
|
|
{
|
|
var localExpires = tokenExpires;
|
|
var localToken = token;
|
|
if (tokenExpires != null && tokenExpires.Value > DateTime.UtcNow && localToken != null)
|
|
return localToken;
|
|
|
|
if (!IsActivated)
|
|
throw new InvalidOperationException("Not activated");
|
|
|
|
await semaphore.WaitAsync();
|
|
try
|
|
{
|
|
localExpires = tokenExpires;
|
|
localToken = token;
|
|
if (tokenExpires != null && tokenExpires.Value > DateTime.UtcNow && localToken != null)
|
|
return localToken;
|
|
|
|
if (!IsActivated)
|
|
throw new InvalidOperationException("Not activated");
|
|
|
|
using (var httpClient = new HttpClient())
|
|
{
|
|
httpClient.BaseAddress = DiscoServiceHelpers.ActivationServiceUrl;
|
|
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
|
|
var timeStamp = DateTime.UtcNow.ToUnixEpoc();
|
|
var iv = new byte[32];
|
|
using (var rng = RandomNumberGenerator.Create())
|
|
rng.GetBytes(iv);
|
|
|
|
var dataStream = new MemoryStream(16 + 16 + 8 + iv.Length);
|
|
dataStream.Write(deploymentId.ToByteArray(), 0, 16);
|
|
dataStream.Write(activationId.Value.ToByteArray(), 0, 16);
|
|
dataStream.Write(BitConverter.GetBytes(timeStamp), 0, 8);
|
|
dataStream.Write(iv, 0, iv.Length);
|
|
byte[] hash;
|
|
using (var hasher = SHA256.Create())
|
|
hash = hasher.ComputeHash(dataStream.ToArray());
|
|
|
|
var signature = ActivationService.SignHash(key, hash);
|
|
|
|
var body = new AuthenticationRequest()
|
|
{
|
|
DeploymentId = deploymentId,
|
|
ActivationId = activationId.Value,
|
|
TimeStamp = timeStamp,
|
|
IV = iv,
|
|
Signature = signature,
|
|
};
|
|
var requestJson = JsonConvert.SerializeObject(body);
|
|
|
|
using (var request = new ByteArrayContent(Encoding.UTF8.GetBytes(requestJson)))
|
|
{
|
|
request.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
|
|
|
var response = await httpClient.PostAsync("/api/authenticate", request);
|
|
|
|
if (response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.BadRequest)
|
|
{
|
|
var responseJson = await response.Content.ReadAsStringAsync();
|
|
var authResponse = JsonConvert.DeserializeObject<AuthenticationResponse>(responseJson);
|
|
|
|
if (authResponse == null)
|
|
throw new InvalidOperationException("Failed to authenticate (empty response)");
|
|
|
|
if (!authResponse.Success)
|
|
throw new InvalidOperationException($"Failed to authenticate ({authResponse.ErrorMessage})");
|
|
|
|
token = authResponse.Token;
|
|
tokenExpires = DateTime.UtcNow.AddSeconds(authResponse.ExpiresInSeconds.Value);
|
|
|
|
return token;
|
|
}
|
|
else
|
|
throw new InvalidOperationException($"Failed to authenticate ({response.StatusCode} {response.ReasonPhrase})");
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
semaphore.Release();
|
|
}
|
|
}
|
|
|
|
internal static void UpdateActivation(DiscoDataContext database)
|
|
{
|
|
semaphore.Wait();
|
|
try
|
|
{
|
|
var config = database.DiscoConfiguration;
|
|
if (config.IsActivated)
|
|
{
|
|
activationId = config.ActivationId;
|
|
key = config.ActivationKey;
|
|
token = null;
|
|
tokenExpires = null;
|
|
|
|
OnlineServicesConnect.QueueStart();
|
|
}
|
|
else
|
|
{
|
|
activationId = null;
|
|
key = null;
|
|
token = null;
|
|
tokenExpires = null;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
semaphore.Release();
|
|
}
|
|
}
|
|
|
|
private class AuthenticationRequest
|
|
{
|
|
public Guid DeploymentId { get; set; }
|
|
public Guid ActivationId { get; set; }
|
|
public long TimeStamp { get; set; }
|
|
public byte[] IV { get; set; }
|
|
public byte[] Signature { get; set; }
|
|
}
|
|
|
|
private class AuthenticationResponse
|
|
{
|
|
public bool Success { get; set; }
|
|
public string Token { get; set; }
|
|
public int? ExpiresInSeconds { get; set; }
|
|
public string ErrorMessage { get; set; }
|
|
}
|
|
}
|
|
}
|