auth apis and api helpers

This commit is contained in:
Kyle Spearrin 2019-04-10 15:03:09 -04:00
parent 579a7e0398
commit 567161d8f3
3 changed files with 195 additions and 13 deletions

View file

@ -5,10 +5,16 @@ namespace Bit.Core.Exceptions
{ {
public class ApiException : Exception public class ApiException : Exception
{ {
public ApiException(ErrorResponse error) public ApiException()
: base("An API error has occurred.") : base("An API error has occurred.")
{ } { }
public ApiException(ErrorResponse error)
: this()
{
Error = error;
}
public ErrorResponse Error { get; set; } public ErrorResponse Error { get; set; }
} }
} }

View file

@ -10,8 +10,35 @@ namespace Bit.Core.Models.Request
public string Email { get; set; } public string Email { get; set; }
public string MasterPasswordHash { get; set; } public string MasterPasswordHash { get; set; }
public string Token { get; set; } public string Token { get; set; }
public TwoFactorProviderType Provider { get; set; } public TwoFactorProviderType? Provider { get; set; }
public bool Remember { get; set; } public bool Remember { get; set; }
public DeviceRequest Device { get; set; } public DeviceRequest Device { get; set; }
public Dictionary<string, string> ToIdentityToken(string clientId)
{
var obj = new Dictionary<string, string>
{
["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;
}
} }
} }

View file

@ -1,12 +1,16 @@
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Models.Response; using Bit.Core.Models.Response;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Bit.Core.Services namespace Bit.Core.Services
@ -16,16 +20,18 @@ namespace Bit.Core.Services
private readonly HttpClient _httpClient = new HttpClient(); private readonly HttpClient _httpClient = new HttpClient();
private readonly ITokenService _tokenService; private readonly ITokenService _tokenService;
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private readonly Func<bool, Task> _logoutCallbackAsync;
private string _deviceType; private string _deviceType;
private bool _usingBaseUrl = false; private bool _usingBaseUrl = false;
public ApiService( public ApiService(
ITokenService tokenService, ITokenService tokenService,
IPlatformUtilsService platformUtilsService) IPlatformUtilsService platformUtilsService,
Func<bool, Task> logoutCallbackAsync)
{ {
_tokenService = tokenService; _tokenService = tokenService;
_platformUtilsService = platformUtilsService; _platformUtilsService = platformUtilsService;
_logoutCallbackAsync = logoutCallbackAsync;
} }
public bool UrlsSet { get; private set; } public bool UrlsSet { get; private set; }
@ -58,21 +64,21 @@ namespace Bit.Core.Services
#region Auth APIs #region Auth APIs
public async Task<Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>> PostIdentityTokenAsync() public async Task<Tuple<IdentityTokenResponse, IdentityTwoFactorResponse>> PostIdentityTokenAsync(
TokenRequest request)
{ {
var request = new HttpRequestMessage var requestMessage = new HttpRequestMessage
{ {
RequestUri = new Uri(string.Concat(IdentityBaseUrl, "/connect/token")), 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"); requestMessage.Headers.Add("Accept", "application/json");
request.Headers.Add("Accept", "application/json"); requestMessage.Headers.Add("Device-Type", _deviceType);
request.Headers.Add("Device-Type", _deviceType);
var response = await _httpClient.SendAsync(request); var response = await _httpClient.SendAsync(requestMessage);
JObject responseJObject = null; JObject responseJObject = null;
if(response.Headers.Contains("content-type") && if(IsJsonResponse(response))
response.Headers.GetValues("content-type").Any(h => h.Contains("application/json")))
{ {
var responseJsonString = await response.Content.ReadAsStringAsync(); var responseJsonString = await response.Content.ReadAsStringAsync();
responseJObject = JObject.Parse(responseJsonString); responseJObject = JObject.Parse(responseJsonString);
@ -97,6 +103,149 @@ namespace Bit.Core.Services
throw new ApiException(new ErrorResponse(responseJObject, response.StatusCode, true)); 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<string> GetActiveBearerTokenAsync()
{
var accessToken = await _tokenService.GetTokenAsync();
if(_tokenService.TokenNeedsRefresh())
{
var tokenResponse = await DoRefreshTokenAsync();
accessToken = tokenResponse.AccessToken;
}
return accessToken;
}
public async Task<TResponse> SendAsync<TRequest, TResponse>(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<TResponse>(responseJsonString);
}
else if(response.IsSuccessStatusCode)
{
var error = await HandleErrorAsync(response, false);
throw new ApiException(error);
}
return (TResponse)(object)null;
}
public async Task<IdentityTokenResponse> 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<string, string>
{
["grant_type"] = "refresh_token",
["client_id"] = decodedToken.GetValue("client_id")?.Value<string>(),
["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<IdentityTokenResponse>(responseJsonString);
await _tokenService.SetTokensAsync(tokenResponse.AccessToken, tokenResponse.RefreshToken);
return tokenResponse;
}
else
{
var error = await HandleErrorAsync(response, true);
throw new ApiException(error);
}
}
private async Task<ErrorResponse> 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 #endregion
} }
} }