From 567161d8f31769431552be2af3a907ee7070f84c Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 10 Apr 2019 15:03:09 -0400 Subject: [PATCH] auth apis and api helpers --- src/Core/Exceptions/ApiException.cs | 8 +- src/Core/Models/Request/TokenRequest.cs | 29 +++- src/Core/Services/ApiService.cs | 171 ++++++++++++++++++++++-- 3 files changed, 195 insertions(+), 13 deletions(-) diff --git a/src/Core/Exceptions/ApiException.cs b/src/Core/Exceptions/ApiException.cs index 2b46d55ec..bf0aa9791 100644 --- a/src/Core/Exceptions/ApiException.cs +++ b/src/Core/Exceptions/ApiException.cs @@ -5,10 +5,16 @@ namespace Bit.Core.Exceptions { public class ApiException : Exception { - public ApiException(ErrorResponse error) + public ApiException() : base("An API error has occurred.") { } + public ApiException(ErrorResponse error) + : this() + { + Error = error; + } + public ErrorResponse Error { get; set; } } } diff --git a/src/Core/Models/Request/TokenRequest.cs b/src/Core/Models/Request/TokenRequest.cs index 7dd8913e6..7a51f9e79 100644 --- a/src/Core/Models/Request/TokenRequest.cs +++ b/src/Core/Models/Request/TokenRequest.cs @@ -10,8 +10,35 @@ namespace Bit.Core.Models.Request public string Email { get; set; } public string MasterPasswordHash { get; set; } public string Token { get; set; } - public TwoFactorProviderType Provider { get; set; } + public TwoFactorProviderType? Provider { get; set; } public bool Remember { get; set; } public DeviceRequest Device { get; set; } + + public Dictionary ToIdentityToken(string clientId) + { + var obj = new Dictionary + { + ["grant_type"] = "password", + ["username"] = Email, + ["password"] = MasterPasswordHash, + ["scope"] = "api offline_access", + ["client_id"] = clientId + }; + if(Device != null) + { + obj.Add("deviceType", ((int)Device.Type).ToString()); + obj.Add("deviceIdentifier", Device.Identifier); + obj.Add("deviceName", Device.Name); + // TODO + // dict.Add("devicePushToken", null); + } + if(!string.IsNullOrWhiteSpace(Token) && Provider != null) + { + obj.Add("twoFactorToken", Token); + obj.Add("twoFactorProvider", ((int)Provider.Value).ToString()); + obj.Add("twoFactorRemember", Remember ? "1" : "0"); + } + return obj; + } } } diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index 118fc4180..382055763 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -1,12 +1,16 @@ using Bit.Core.Abstractions; using Bit.Core.Exceptions; using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; using Bit.Core.Models.Response; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Text; using System.Threading.Tasks; namespace Bit.Core.Services @@ -16,16 +20,18 @@ namespace Bit.Core.Services private readonly HttpClient _httpClient = new HttpClient(); private readonly ITokenService _tokenService; private readonly IPlatformUtilsService _platformUtilsService; - + private readonly Func _logoutCallbackAsync; private string _deviceType; private bool _usingBaseUrl = false; public ApiService( ITokenService tokenService, - IPlatformUtilsService platformUtilsService) + IPlatformUtilsService platformUtilsService, + Func logoutCallbackAsync) { _tokenService = tokenService; _platformUtilsService = platformUtilsService; + _logoutCallbackAsync = logoutCallbackAsync; } public bool UrlsSet { get; private set; } @@ -58,21 +64,21 @@ namespace Bit.Core.Services #region Auth APIs - public async Task> PostIdentityTokenAsync() + public async Task> PostIdentityTokenAsync( + TokenRequest request) { - var request = new HttpRequestMessage + var requestMessage = new HttpRequestMessage { RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")), - Method = HttpMethod.Post + Method = HttpMethod.Post, + Content = new FormUrlEncodedContent(request.ToIdentityToken(_platformUtilsService.IdentityClientId)) }; - 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); + requestMessage.Headers.Add("Accept", "application/json"); + requestMessage.Headers.Add("Device-Type", _deviceType); - var response = await _httpClient.SendAsync(request); + var response = await _httpClient.SendAsync(requestMessage); JObject responseJObject = null; - if(response.Headers.Contains("content-type") && - response.Headers.GetValues("content-type").Any(h => h.Contains("application/json"))) + if(IsJsonResponse(response)) { var responseJsonString = await response.Content.ReadAsStringAsync(); responseJObject = JObject.Parse(responseJsonString); @@ -97,6 +103,149 @@ namespace Bit.Core.Services throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true)); } + public async Task RefreshIdentityTokenAsync() + { + try + { + await DoRefreshTokenAsync(); + } + catch + { + throw new ApiException(); + } + } + + #endregion + + + + #region Helpers + + public async Task GetActiveBearerTokenAsync() + { + var accessToken = await _tokenService.GetTokenAsync(); + if(_tokenService.TokenNeedsRefresh()) + { + var tokenResponse = await DoRefreshTokenAsync(); + accessToken = tokenResponse.AccessToken; + } + return accessToken; + } + + public async Task SendAsync(HttpMethod method, string path, TRequest body, + bool authed, bool hasResponse) + { + var requestMessage = new HttpRequestMessage + { + Method = method, + RequestUri = new Uri(string.Concat(ApiBaseUrl, path)), + }; + + if(body != null) + { + var bodyType = body.GetType(); + if(bodyType == typeof(string)) + { + requestMessage.Content = new StringContent((object)bodyType as string, Encoding.UTF8, + "application/x-www-form-urlencoded; charset=utf-8"); + } + else if(false) + { + // TODO: form data content + } + else + { + requestMessage.Content = new StringContent(JsonConvert.SerializeObject(body), + Encoding.UTF8, "application/json"); + } + } + + requestMessage.Headers.Add("Device-Type", _deviceType); + if(authed) + { + var authHeader = await GetActiveBearerTokenAsync(); + requestMessage.Headers.Add("Authorization", string.Concat("Bearer ", authHeader)); + } + if(hasResponse) + { + requestMessage.Headers.Add("Accept", "application/json"); + } + + var response = await _httpClient.SendAsync(requestMessage); + if(hasResponse && response.IsSuccessStatusCode) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseJsonString); + } + else if(response.IsSuccessStatusCode) + { + var error = await HandleErrorAsync(response, false); + throw new ApiException(error); + } + return (TResponse)(object)null; + } + + public async Task DoRefreshTokenAsync() + { + var refreshToken = await _tokenService.GetRefreshTokenAsync(); + if(string.IsNullOrWhiteSpace(refreshToken)) + { + throw new ApiException(); + } + + var decodedToken = _tokenService.DecodeToken(); + var requestMessage = new HttpRequestMessage + { + RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")), + Method = HttpMethod.Post, + Content = new FormUrlEncodedContent(new Dictionary + { + ["grant_type"] = "refresh_token", + ["client_id"] = decodedToken.GetValue("client_id")?.Value(), + ["refresh_token"] = refreshToken + }) + }; + requestMessage.Headers.Add("Accept", "application/json"); + requestMessage.Headers.Add("Device-Type", _deviceType); + + var response = await _httpClient.SendAsync(requestMessage); + if(response.IsSuccessStatusCode) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + var tokenResponse = JsonConvert.DeserializeObject(responseJsonString); + await _tokenService.SetTokensAsync(tokenResponse.AccessToken, tokenResponse.RefreshToken); + return tokenResponse; + } + else + { + var error = await HandleErrorAsync(response, true); + throw new ApiException(error); + } + } + + private async Task HandleErrorAsync(HttpResponseMessage response, bool tokenError) + { + if((tokenError && response.StatusCode == HttpStatusCode.BadRequest) || + response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden) + { + await _logoutCallbackAsync(true); + return null; + } + JObject responseJObject = null; + if(IsJsonResponse(response)) + { + var responseJsonString = await response.Content.ReadAsStringAsync(); + responseJObject = JObject.Parse(responseJsonString); + } + return new ErrorResponse(responseJObject, response.StatusCode, tokenError); + } + + private bool IsJsonResponse(HttpResponseMessage response) + { + return response.Headers.Contains("content-type") && + response.Headers.GetValues("content-type").Any(h => h.Contains("application/json")); + } + #endregion } }