HandleTokenStateAsync before each API call for refresh and auth bearer migration

This commit is contained in:
Kyle Spearrin 2017-02-04 23:31:37 -05:00
parent 8ae95c4e30
commit 74239521cd
11 changed files with 216 additions and 30 deletions

View file

@ -11,7 +11,7 @@ namespace Bit.App.Abstractions
DateTime TokenExpiration { get; } DateTime TokenExpiration { get; }
bool TokenExpired { get; } bool TokenExpired { get; }
TimeSpan TokenTimeRemaining { get; } TimeSpan TokenTimeRemaining { get; }
bool TokenNeedseRefresh { get; } bool TokenNeedsRefresh { get; }
string TokenUserId { get; } string TokenUserId { get; }
string TokenEmail { get; } string TokenEmail { get; }
string TokenName { get; } string TokenName { get; }

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
namespace Bit.App.Models.Api namespace Bit.App.Models.Api
{ {
@ -8,6 +9,8 @@ namespace Bit.App.Models.Api
public string MasterPasswordHash { get; set; } public string MasterPasswordHash { get; set; }
public string Token { get; set; } public string Token { get; set; }
public int? Provider { get; set; } public int? Provider { get; set; }
[Obsolete]
public string OldAuthBearer { get; set; }
public DeviceRequest Device { get; set; } public DeviceRequest Device { get; set; }
public IDictionary<string, string> ToIdentityTokenRequest() public IDictionary<string, string> ToIdentityTokenRequest()
@ -21,6 +24,11 @@ namespace Bit.App.Models.Api
{ "client_id", "mobile" } { "client_id", "mobile" }
}; };
if(OldAuthBearer != null)
{
dict.Add("OldAuthBearer", OldAuthBearer);
}
if(Device != null) if(Device != null)
{ {
dict.Add("DeviceType", Device.Type.ToString()); dict.Add("DeviceType", Device.Type.ToString());

View file

@ -12,8 +12,9 @@ namespace Bit.App.Repositories
{ {
public AccountsApiRepository( public AccountsApiRepository(
IConnectivity connectivity, IConnectivity connectivity,
IHttpService httpService) IHttpService httpService,
: base(connectivity, httpService) ITokenService tokenService)
: base(connectivity, httpService, tokenService)
{ } { }
protected override string ApiRoute => "accounts"; protected override string ApiRoute => "accounts";

View file

@ -17,8 +17,9 @@ namespace Bit.App.Repositories
{ {
public ApiRepository( public ApiRepository(
IConnectivity connectivity, IConnectivity connectivity,
IHttpService httpService) IHttpService httpService,
: base(connectivity, httpService) ITokenService tokenService)
: base(connectivity, httpService, tokenService)
{ } { }
public virtual async Task<ApiResult<TResponse>> GetByIdAsync(TId id) public virtual async Task<ApiResult<TResponse>> GetByIdAsync(TId id)
@ -28,6 +29,12 @@ namespace Bit.App.Repositories
return HandledNotConnected<TResponse>(); return HandledNotConnected<TResponse>();
} }
var tokenStateResponse = await HandleTokenStateAsync<TResponse>();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.Client) using(var client = HttpService.Client)
{ {
var requestMessage = new TokenHttpRequestMessage() var requestMessage = new TokenHttpRequestMessage()
@ -62,6 +69,12 @@ namespace Bit.App.Repositories
return HandledNotConnected<ListResponse<TResponse>>(); return HandledNotConnected<ListResponse<TResponse>>();
} }
var tokenStateResponse = await HandleTokenStateAsync<ListResponse<TResponse>>();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.Client) using(var client = HttpService.Client)
{ {
var requestMessage = new TokenHttpRequestMessage() var requestMessage = new TokenHttpRequestMessage()
@ -96,6 +109,12 @@ namespace Bit.App.Repositories
return HandledNotConnected<TResponse>(); return HandledNotConnected<TResponse>();
} }
var tokenStateResponse = await HandleTokenStateAsync<TResponse>();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.Client) using(var client = HttpService.Client)
{ {
var requestMessage = new TokenHttpRequestMessage(requestObj) var requestMessage = new TokenHttpRequestMessage(requestObj)
@ -130,6 +149,12 @@ namespace Bit.App.Repositories
return HandledNotConnected<TResponse>(); return HandledNotConnected<TResponse>();
} }
var tokenStateResponse = await HandleTokenStateAsync<TResponse>();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.Client) using(var client = HttpService.Client)
{ {
var requestMessage = new TokenHttpRequestMessage(requestObj) var requestMessage = new TokenHttpRequestMessage(requestObj)
@ -164,6 +189,12 @@ namespace Bit.App.Repositories
return HandledNotConnected(); return HandledNotConnected();
} }
var tokenStateResponse = await HandleTokenStateAsync();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.Client) using(var client = HttpService.Client)
{ {
var requestMessage = new TokenHttpRequestMessage() var requestMessage = new TokenHttpRequestMessage()

View file

@ -6,42 +6,153 @@ using Bit.App.Models.Api;
using Newtonsoft.Json; using Newtonsoft.Json;
using Plugin.Connectivity.Abstractions; using Plugin.Connectivity.Abstractions;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using System.Net;
using XLabs.Ioc;
namespace Bit.App.Repositories namespace Bit.App.Repositories
{ {
public abstract class BaseApiRepository public abstract class BaseApiRepository
{ {
public BaseApiRepository(IConnectivity connectivity, IHttpService httpService) public BaseApiRepository(
IConnectivity connectivity,
IHttpService httpService,
ITokenService tokenService)
{ {
Connectivity = connectivity; Connectivity = connectivity;
HttpService = httpService; HttpService = httpService;
TokenService = tokenService;
} }
protected IConnectivity Connectivity { get; private set; } protected IConnectivity Connectivity { get; private set; }
protected IHttpService HttpService { get; private set; } protected IHttpService HttpService { get; private set; }
protected ITokenService TokenService { get; private set; }
protected abstract string ApiRoute { get; } protected abstract string ApiRoute { get; }
protected async Task<ApiResult> HandleTokenStateAsync()
{
return await HandleTokenStateAsync(
() => ApiResult.Success(HttpStatusCode.OK),
() => HandledWebException(),
(r) => HandleErrorAsync(r));
}
protected async Task<ApiResult<T>> HandleTokenStateAsync<T>()
{
return await HandleTokenStateAsync(
() => ApiResult<T>.Success(default(T), HttpStatusCode.OK),
() => HandledWebException<T>(),
(r) => HandleErrorAsync<T>(r));
}
private async Task<T> HandleTokenStateAsync<T>(Func<T> success, Func<T> webException,
Func<HttpResponseMessage, Task<T>> error)
{
if(!string.IsNullOrWhiteSpace(TokenService.AuthBearer) && string.IsNullOrWhiteSpace(TokenService.Token))
{
// Migrate from old auth bearer to new access token
var deviceInfoService = Resolver.Resolve<IDeviceInfoService>();
var appIdService = Resolver.Resolve<IAppIdService>();
using(var client = HttpService.Client)
{
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(client.BaseAddress, string.Concat(ApiRoute, "/token")),
Content = new FormUrlEncodedContent(new TokenRequest
{
Email = "abcdefgh",
MasterPasswordHash = "abcdefgh",
OldAuthBearer = TokenService.AuthBearer,
Device = new DeviceRequest(appIdService, deviceInfoService)
}.ToIdentityTokenRequest())
};
try
{
var response = await client.SendAsync(requestMessage).ConfigureAwait(false);
if(!response.IsSuccessStatusCode)
{
return await error.Invoke(response).ConfigureAwait(false);
}
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
TokenService.Token = tokenResponse.AccessToken;
TokenService.RefreshToken = tokenResponse.RefreshToken;
TokenService.AuthBearer = null;
}
catch(WebException)
{
return webException.Invoke();
}
}
}
else if(TokenService.TokenNeedsRefresh && !string.IsNullOrWhiteSpace(TokenService.RefreshToken))
{
using(var client = HttpService.Client)
{
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(client.BaseAddress, string.Concat(ApiRoute, "/token")),
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "refresh_token" },
{ "client_id", "mobile" },
{ "refresh_token", TokenService.RefreshToken }
})
};
try
{
var response = await client.SendAsync(requestMessage).ConfigureAwait(false);
if(!response.IsSuccessStatusCode)
{
return await error.Invoke(response).ConfigureAwait(false);
}
var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(responseContent);
TokenService.Token = tokenResponse.AccessToken;
TokenService.RefreshToken = tokenResponse.RefreshToken;
}
catch(WebException)
{
return webException.Invoke();
}
}
}
else if(!string.IsNullOrWhiteSpace(TokenService.AuthBearer))
{
TokenService.AuthBearer = null;
}
return success.Invoke();
}
protected ApiResult HandledNotConnected() protected ApiResult HandledNotConnected()
{ {
return ApiResult.Failed(System.Net.HttpStatusCode.RequestTimeout, return ApiResult.Failed(HttpStatusCode.RequestTimeout,
new ApiError { Message = "Not connected to the internet." }); new ApiError { Message = "Not connected to the internet." });
} }
protected ApiResult<T> HandledNotConnected<T>() protected ApiResult<T> HandledNotConnected<T>()
{ {
return ApiResult<T>.Failed(System.Net.HttpStatusCode.RequestTimeout, return ApiResult<T>.Failed(HttpStatusCode.RequestTimeout,
new ApiError { Message = "Not connected to the internet." }); new ApiError { Message = "Not connected to the internet." });
} }
protected ApiResult HandledWebException() protected ApiResult HandledWebException()
{ {
return ApiResult.Failed(System.Net.HttpStatusCode.BadGateway, return ApiResult.Failed(HttpStatusCode.BadGateway,
new ApiError { Message = "There is a problem connecting to the server." }); new ApiError { Message = "There is a problem connecting to the server." });
} }
protected ApiResult<T> HandledWebException<T>() protected ApiResult<T> HandledWebException<T>()
{ {
return ApiResult<T>.Failed(System.Net.HttpStatusCode.BadGateway, return ApiResult<T>.Failed(HttpStatusCode.BadGateway,
new ApiError { Message = "There is a problem connecting to the server." }); new ApiError { Message = "There is a problem connecting to the server." });
} }

View file

@ -14,8 +14,9 @@ namespace Bit.App.Repositories
{ {
public CipherApiRepository( public CipherApiRepository(
IConnectivity connectivity, IConnectivity connectivity,
IHttpService httpService) IHttpService httpService,
: base(connectivity, httpService) ITokenService tokenService)
: base(connectivity, httpService, tokenService)
{ } { }
protected override string ApiRoute => "ciphers"; protected override string ApiRoute => "ciphers";
@ -27,6 +28,12 @@ namespace Bit.App.Repositories
return HandledNotConnected<CipherResponse>(); return HandledNotConnected<CipherResponse>();
} }
var tokenStateResponse = await HandleTokenStateAsync<CipherResponse>();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.Client) using(var client = HttpService.Client)
{ {
var requestMessage = new TokenHttpRequestMessage() var requestMessage = new TokenHttpRequestMessage()
@ -61,6 +68,12 @@ namespace Bit.App.Repositories
return HandledNotConnected<ListResponse<CipherResponse>>(); return HandledNotConnected<ListResponse<CipherResponse>>();
} }
var tokenStateResponse = await HandleTokenStateAsync<ListResponse<CipherResponse>>();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.Client) using(var client = HttpService.Client)
{ {
var requestMessage = new TokenHttpRequestMessage() var requestMessage = new TokenHttpRequestMessage()
@ -95,6 +108,12 @@ namespace Bit.App.Repositories
return HandledNotConnected<CipherHistoryResponse>(); return HandledNotConnected<CipherHistoryResponse>();
} }
var tokenStateResponse = await HandleTokenStateAsync<CipherHistoryResponse>();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.Client) using(var client = HttpService.Client)
{ {
var requestMessage = new TokenHttpRequestMessage() var requestMessage = new TokenHttpRequestMessage()

View file

@ -13,8 +13,9 @@ namespace Bit.App.Repositories
{ {
public ConnectApiRepository( public ConnectApiRepository(
IConnectivity connectivity, IConnectivity connectivity,
IHttpService httpService) IHttpService httpService,
: base(connectivity, httpService) ITokenService tokenService)
: base(connectivity, httpService, tokenService)
{ } { }
protected override string ApiRoute => "connect"; protected override string ApiRoute => "connect";

View file

@ -13,8 +13,9 @@ namespace Bit.App.Repositories
{ {
public DeviceApiRepository( public DeviceApiRepository(
IConnectivity connectivity, IConnectivity connectivity,
IHttpService httpService) IHttpService httpService,
: base(connectivity, httpService) ITokenService tokenService)
: base(connectivity, httpService, tokenService)
{ } { }
protected override string ApiRoute => "devices"; protected override string ApiRoute => "devices";
@ -26,6 +27,12 @@ namespace Bit.App.Repositories
return HandledNotConnected(); return HandledNotConnected();
} }
var tokenStateResponse = await HandleTokenStateAsync();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.Client) using(var client = HttpService.Client)
{ {
var requestMessage = new TokenHttpRequestMessage(request) var requestMessage = new TokenHttpRequestMessage(request)

View file

@ -14,8 +14,9 @@ namespace Bit.App.Repositories
{ {
public FolderApiRepository( public FolderApiRepository(
IConnectivity connectivity, IConnectivity connectivity,
IHttpService httpService) IHttpService httpService,
: base(connectivity, httpService) ITokenService tokenService)
: base(connectivity, httpService, tokenService)
{ } { }
protected override string ApiRoute => "folders"; protected override string ApiRoute => "folders";
@ -27,6 +28,12 @@ namespace Bit.App.Repositories
return HandledNotConnected<ListResponse<FolderResponse>>(); return HandledNotConnected<ListResponse<FolderResponse>>();
} }
var tokenStateResponse = await HandleTokenStateAsync<ListResponse<FolderResponse>>();
if(!tokenStateResponse.Succeeded)
{
return tokenStateResponse;
}
using(var client = HttpService.Client) using(var client = HttpService.Client)
{ {
var requestMessage = new TokenHttpRequestMessage() var requestMessage = new TokenHttpRequestMessage()

View file

@ -13,8 +13,9 @@ namespace Bit.App.Repositories
{ {
public LoginApiRepository( public LoginApiRepository(
IConnectivity connectivity, IConnectivity connectivity,
IHttpService httpService) IHttpService httpService,
: base(connectivity, httpService) ITokenService tokenService)
: base(connectivity, httpService, tokenService)
{ } { }
protected override string ApiRoute => "sites"; protected override string ApiRoute => "sites";

View file

@ -2,6 +2,7 @@
using Bit.App.Abstractions; using Bit.App.Abstractions;
using System.Text; using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Bit.App.Services namespace Bit.App.Services
{ {
@ -67,22 +68,21 @@ namespace Bit.App.Services
get get
{ {
var decoded = DecodeToken(); var decoded = DecodeToken();
long exp = 0; if(decoded?["exp"] == null)
if(decoded?.exp != null || !long.TryParse(decoded.exp, out exp))
{ {
throw new InvalidOperationException("No exp in token."); throw new InvalidOperationException("No exp in token.");
} }
return _epoc.AddSeconds(Convert.ToDouble(exp)); return _epoc.AddSeconds(Convert.ToDouble(decoded["exp"].Value<long>()));
} }
} }
public bool TokenExpired => DateTime.UtcNow < TokenExpiration; public bool TokenExpired => DateTime.UtcNow < TokenExpiration;
public TimeSpan TokenTimeRemaining => TokenExpiration - DateTime.UtcNow; public TimeSpan TokenTimeRemaining => TokenExpiration - DateTime.UtcNow;
public bool TokenNeedseRefresh => TokenTimeRemaining.TotalMinutes < 5; public bool TokenNeedsRefresh => TokenTimeRemaining.TotalMinutes < 5;
public string TokenUserId => DecodeToken()?.sub; public string TokenUserId => DecodeToken()?["sub"].Value<string>();
public string TokenEmail => DecodeToken()?.email; public string TokenEmail => DecodeToken()?["email"].Value<string>();
public string TokenName => DecodeToken()?.name; public string TokenName => DecodeToken()?["name"].Value<string>();
public string RefreshToken public string RefreshToken
{ {
@ -152,7 +152,7 @@ namespace Bit.App.Services
} }
} }
public dynamic DecodeToken() public JObject DecodeToken()
{ {
if(_decodedToken != null) if(_decodedToken != null)
{ {
@ -176,7 +176,7 @@ namespace Bit.App.Services
throw new InvalidOperationException($"{nameof(Token)} must have 3 parts"); throw new InvalidOperationException($"{nameof(Token)} must have 3 parts");
} }
_decodedToken = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(decodedBytes, 0, decodedBytes.Length)); _decodedToken = JObject.Parse(Encoding.UTF8.GetString(decodedBytes, 0, decodedBytes.Length));
return _decodedToken; return _decodedToken;
} }