diff --git a/src/App/Services/MobilePlatformUtilsService.cs b/src/App/Services/MobilePlatformUtilsService.cs index f45bc8a79..5a78065b9 100644 --- a/src/App/Services/MobilePlatformUtilsService.cs +++ b/src/App/Services/MobilePlatformUtilsService.cs @@ -1,5 +1,6 @@ using Bit.App.Abstractions; using Bit.App.Models; +using Bit.Core.Abstractions; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -8,7 +9,7 @@ using Xamarin.Forms; namespace Bit.App.Services { - public class MobilePlatformUtilsService + public class MobilePlatformUtilsService : IPlatformUtilsService { private static readonly Random _random = new Random(); diff --git a/src/Core/Abstractions/IPlatformUtilsService.cs b/src/Core/Abstractions/IPlatformUtilsService.cs new file mode 100644 index 000000000..1c21ad28d --- /dev/null +++ b/src/Core/Abstractions/IPlatformUtilsService.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Enums; + +namespace Bit.Core.Abstractions +{ + public interface IPlatformUtilsService + { + string IdentityClientId { get; } + + Task CopyToClipboardAsync(string text, Dictionary options = null); + string GetApplicationVersion(); + DeviceType GetDevice(); + string GetDeviceString(); + bool IsDev(); + bool IsSelfHost(); + bool IsViewOpen(); + void LaunchUri(string uri, Dictionary options = null); + int? LockTimeout(); + Task ReadFromClipboardAsync(Dictionary options = null); + void SaveFile(); + Task ShowDialogAsync(string text, string title = null, string confirmText = null, + string cancelText = null, string type = null); + void ShowToast(string type, string title, string text, Dictionary options = null); + void ShowToast(string type, string title, string[] text, Dictionary options = null); + bool SupportsU2f(); + } +} \ No newline at end of file diff --git a/src/Core/Exceptions/ApiException.cs b/src/Core/Exceptions/ApiException.cs new file mode 100644 index 000000000..2b46d55ec --- /dev/null +++ b/src/Core/Exceptions/ApiException.cs @@ -0,0 +1,14 @@ +using Bit.Core.Models.Response; +using System; + +namespace Bit.Core.Exceptions +{ + public class ApiException : Exception + { + public ApiException(ErrorResponse error) + : base("An API error has occurred.") + { } + + public ErrorResponse Error { get; set; } + } +} diff --git a/src/Core/Models/Domain/EnvironmentUrls.cs b/src/Core/Models/Domain/EnvironmentUrls.cs new file mode 100644 index 000000000..6f6b43577 --- /dev/null +++ b/src/Core/Models/Domain/EnvironmentUrls.cs @@ -0,0 +1,9 @@ +namespace Bit.Core.Models.Domain +{ + public class EnvironmentUrls + { + public string Base { get; set; } + public string Api { get; set; } + public string Identity { get; set; } + } +} diff --git a/src/Core/Models/Request/DeviceRequestModels.cs b/src/Core/Models/Request/DeviceRequestModels.cs new file mode 100644 index 000000000..ecc9bf90c --- /dev/null +++ b/src/Core/Models/Request/DeviceRequestModels.cs @@ -0,0 +1,21 @@ +using Bit.Core.Abstractions; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Request +{ + public class DeviceRequest + { + public DeviceRequest(string appId, IPlatformUtilsService platformUtilsService) + { + Type = platformUtilsService.GetDevice(); + Name = platformUtilsService.GetDeviceString(); + Identifier = appId; + PushToken = null; // TODO? + } + + public DeviceType? Type { get; set; } + public string Name { get; set; } + public string Identifier { get; set; } + public string PushToken { get; set; } + } +} diff --git a/src/Core/Models/Request/TokenRequest.cs b/src/Core/Models/Request/TokenRequest.cs new file mode 100644 index 000000000..7dd8913e6 --- /dev/null +++ b/src/Core/Models/Request/TokenRequest.cs @@ -0,0 +1,17 @@ +using Bit.Core.Enums; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Bit.Core.Models.Request +{ + public class TokenRequest + { + public string Email { get; set; } + public string MasterPasswordHash { get; set; } + public string Token { get; set; } + public TwoFactorProviderType Provider { get; set; } + public bool Remember { get; set; } + public DeviceRequest Device { get; set; } + } +} diff --git a/src/Core/Models/Response/ErrorResponse.cs b/src/Core/Models/Response/ErrorResponse.cs new file mode 100644 index 000000000..188134dae --- /dev/null +++ b/src/Core/Models/Response/ErrorResponse.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; + +namespace Bit.Core.Models.Response +{ + public class ErrorResponse + { + public ErrorResponse(JObject response, HttpStatusCode status, bool identityResponse = false) + { + JObject errorModel = null; + if(response != null) + { + var responseErrorModel = response.GetValue("ErrorModel", StringComparison.OrdinalIgnoreCase); + if(responseErrorModel != null && identityResponse) + { + errorModel = responseErrorModel.Value(); ; + } + else + { + errorModel = response; + } + } + if(errorModel != null) + { + Message = errorModel.GetValue("Message", StringComparison.OrdinalIgnoreCase)?.Value(); + ValidationErrors = errorModel.GetValue("ValidationErrors", StringComparison.OrdinalIgnoreCase) + ?.Value>>(); + } + else + { + if((int)status == 429) + { + Message = "Rate limit exceeded. Try again later."; + } + } + StatusCode = status; + } + + public string Message { get; set; } + public Dictionary> ValidationErrors { get; set; } + public HttpStatusCode StatusCode { get; set; } + + public string GetSingleMessage() + { + if(ValidationErrors == null) + { + return Message; + } + foreach(var error in ValidationErrors) + { + if(error.Value?.Any() ?? false) + { + return error.Value[0]; + } + } + return Message; + } + } +} diff --git a/src/Core/Models/Response/IdentityTokenResponse.cs b/src/Core/Models/Response/IdentityTokenResponse.cs new file mode 100644 index 000000000..c2c8e5ee9 --- /dev/null +++ b/src/Core/Models/Response/IdentityTokenResponse.cs @@ -0,0 +1,13 @@ +namespace Bit.Core.Models.Response +{ + public class IdentityTokenResponse + { + public string AccessToken { get; set; } + public string ExpiresIn { get; set; } + public string RefreshToken { get; set; } + public string TokenType { get; set; } + public string PrivateKey { get; set; } + public string Key { get; set; } + public string TwoFactorToken { get; set; } + } +} diff --git a/src/Core/Models/Response/IdentityTwoFactorResponse.cs b/src/Core/Models/Response/IdentityTwoFactorResponse.cs new file mode 100644 index 000000000..82f082e9e --- /dev/null +++ b/src/Core/Models/Response/IdentityTwoFactorResponse.cs @@ -0,0 +1,11 @@ +using Bit.Core.Enums; +using System.Collections.Generic; + +namespace Bit.Core.Models.Response +{ + public class IdentityTwoFactorResponse + { + public List TwoFactorProviders { get; set; } + public Dictionary> TwoFactorProviders2 { get; set; } + } +} diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs new file mode 100644 index 000000000..118fc4180 --- /dev/null +++ b/src/Core/Services/ApiService.cs @@ -0,0 +1,102 @@ +using Bit.Core.Abstractions; +using Bit.Core.Exceptions; +using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; +using Newtonsoft.Json.Linq; +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Bit.Core.Services +{ + public class ApiService + { + private readonly HttpClient _httpClient = new HttpClient(); + private readonly ITokenService _tokenService; + private readonly IPlatformUtilsService _platformUtilsService; + + private string _deviceType; + private bool _usingBaseUrl = false; + + public ApiService( + ITokenService tokenService, + IPlatformUtilsService platformUtilsService) + { + _tokenService = tokenService; + _platformUtilsService = platformUtilsService; + } + + public bool UrlsSet { get; private set; } + public string ApiBaseUrl { get; set; } + public string IdentityBaseUrl { get; set; } + + public void SetUrls(EnvironmentUrls urls) + { + UrlsSet = true; + if(!string.IsNullOrWhiteSpace(urls.Base)) + { + _usingBaseUrl = true; + ApiBaseUrl = urls.Base + "/api"; + IdentityBaseUrl = urls.Base + "/identity"; + return; + } + if(!string.IsNullOrWhiteSpace(urls.Api) && !string.IsNullOrWhiteSpace(urls.Identity)) + { + ApiBaseUrl = urls.Api; + IdentityBaseUrl = urls.Identity; + return; + } + // Local Dev + //ApiBaseUrl = "http://localhost:4000"; + //IdentityBaseUrl = "http://localhost:33656"; + // Production + ApiBaseUrl = "https://api.bitwarden.com"; + IdentityBaseUrl = "https://identity.bitwarden.com"; + } + + #region Auth APIs + + public async Task> PostIdentityTokenAsync() + { + var request = new HttpRequestMessage + { + RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")), + Method = HttpMethod.Post + }; + request.Headers.Add("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + request.Headers.Add("Accept", "application/json"); + request.Headers.Add("Device-Type", _deviceType); + + var response = await _httpClient.SendAsync(request); + JObject responseJObject = null; + if(response.Headers.Contains("content-type") && + response.Headers.GetValues("content-type").Any(h => h.Contains("application/json"))) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + responseJObject = JObject.Parse(responseJsonString); + } + + if(responseJObject != null) + { + if(response.IsSuccessStatusCode) + { + return new Tuple( + responseJObject.ToObject(), null); + } + else if(response.StatusCode == HttpStatusCode.BadRequest && + responseJObject.ContainsKey("TwoFactorProviders2") && + responseJObject["TwoFactorProviders2"] != null && + responseJObject["TwoFactorProviders2"].HasValues) + { + return new Tuple( + null, responseJObject.ToObject()); + } + } + throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true)); + } + + #endregion + } +}