diff --git a/src/App/Abstractions/Services/ITokenService.cs b/src/App/Abstractions/Services/ITokenService.cs index 032f5692c..7e6086175 100644 --- a/src/App/Abstractions/Services/ITokenService.cs +++ b/src/App/Abstractions/Services/ITokenService.cs @@ -11,7 +11,7 @@ namespace Bit.App.Abstractions DateTime TokenExpiration { get; } bool TokenExpired { get; } TimeSpan TokenTimeRemaining { get; } - bool TokenNeedseRefresh { get; } + bool TokenNeedsRefresh { get; } string TokenUserId { get; } string TokenEmail { get; } string TokenName { get; } diff --git a/src/App/Models/Api/Request/TokenRequest.cs b/src/App/Models/Api/Request/TokenRequest.cs index 98bb3f1ec..96ba1329b 100644 --- a/src/App/Models/Api/Request/TokenRequest.cs +++ b/src/App/Models/Api/Request/TokenRequest.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Bit.App.Models.Api { @@ -8,6 +9,8 @@ namespace Bit.App.Models.Api public string MasterPasswordHash { get; set; } public string Token { get; set; } public int? Provider { get; set; } + [Obsolete] + public string OldAuthBearer { get; set; } public DeviceRequest Device { get; set; } public IDictionary ToIdentityTokenRequest() @@ -21,6 +24,11 @@ namespace Bit.App.Models.Api { "client_id", "mobile" } }; + if(OldAuthBearer != null) + { + dict.Add("OldAuthBearer", OldAuthBearer); + } + if(Device != null) { dict.Add("DeviceType", Device.Type.ToString()); diff --git a/src/App/Repositories/AccountsApiRepository.cs b/src/App/Repositories/AccountsApiRepository.cs index b8536c6c7..ab05c5cb0 100644 --- a/src/App/Repositories/AccountsApiRepository.cs +++ b/src/App/Repositories/AccountsApiRepository.cs @@ -12,8 +12,9 @@ namespace Bit.App.Repositories { public AccountsApiRepository( IConnectivity connectivity, - IHttpService httpService) - : base(connectivity, httpService) + IHttpService httpService, + ITokenService tokenService) + : base(connectivity, httpService, tokenService) { } protected override string ApiRoute => "accounts"; diff --git a/src/App/Repositories/ApiRepository.cs b/src/App/Repositories/ApiRepository.cs index 6e916c354..de76cf17d 100644 --- a/src/App/Repositories/ApiRepository.cs +++ b/src/App/Repositories/ApiRepository.cs @@ -17,8 +17,9 @@ namespace Bit.App.Repositories { public ApiRepository( IConnectivity connectivity, - IHttpService httpService) - : base(connectivity, httpService) + IHttpService httpService, + ITokenService tokenService) + : base(connectivity, httpService, tokenService) { } public virtual async Task> GetByIdAsync(TId id) @@ -28,6 +29,12 @@ namespace Bit.App.Repositories return HandledNotConnected(); } + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse.Succeeded) + { + return tokenStateResponse; + } + using(var client = HttpService.Client) { var requestMessage = new TokenHttpRequestMessage() @@ -62,6 +69,12 @@ namespace Bit.App.Repositories return HandledNotConnected>(); } + var tokenStateResponse = await HandleTokenStateAsync>(); + if(!tokenStateResponse.Succeeded) + { + return tokenStateResponse; + } + using(var client = HttpService.Client) { var requestMessage = new TokenHttpRequestMessage() @@ -96,6 +109,12 @@ namespace Bit.App.Repositories return HandledNotConnected(); } + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse.Succeeded) + { + return tokenStateResponse; + } + using(var client = HttpService.Client) { var requestMessage = new TokenHttpRequestMessage(requestObj) @@ -130,6 +149,12 @@ namespace Bit.App.Repositories return HandledNotConnected(); } + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse.Succeeded) + { + return tokenStateResponse; + } + using(var client = HttpService.Client) { var requestMessage = new TokenHttpRequestMessage(requestObj) @@ -164,6 +189,12 @@ namespace Bit.App.Repositories return HandledNotConnected(); } + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse.Succeeded) + { + return tokenStateResponse; + } + using(var client = HttpService.Client) { var requestMessage = new TokenHttpRequestMessage() diff --git a/src/App/Repositories/BaseApiRepository.cs b/src/App/Repositories/BaseApiRepository.cs index 429b1dfea..45c3c304e 100644 --- a/src/App/Repositories/BaseApiRepository.cs +++ b/src/App/Repositories/BaseApiRepository.cs @@ -6,42 +6,153 @@ using Bit.App.Models.Api; using Newtonsoft.Json; using Plugin.Connectivity.Abstractions; using Bit.App.Abstractions; +using System.Net; +using XLabs.Ioc; namespace Bit.App.Repositories { public abstract class BaseApiRepository { - public BaseApiRepository(IConnectivity connectivity, IHttpService httpService) + public BaseApiRepository( + IConnectivity connectivity, + IHttpService httpService, + ITokenService tokenService) { Connectivity = connectivity; HttpService = httpService; + TokenService = tokenService; } protected IConnectivity Connectivity { get; private set; } protected IHttpService HttpService { get; private set; } + protected ITokenService TokenService { get; private set; } protected abstract string ApiRoute { get; } + protected async Task HandleTokenStateAsync() + { + return await HandleTokenStateAsync( + () => ApiResult.Success(HttpStatusCode.OK), + () => HandledWebException(), + (r) => HandleErrorAsync(r)); + } + + protected async Task> HandleTokenStateAsync() + { + return await HandleTokenStateAsync( + () => ApiResult.Success(default(T), HttpStatusCode.OK), + () => HandledWebException(), + (r) => HandleErrorAsync(r)); + } + + private async Task HandleTokenStateAsync(Func success, Func webException, + Func> error) + { + if(!string.IsNullOrWhiteSpace(TokenService.AuthBearer) && string.IsNullOrWhiteSpace(TokenService.Token)) + { + // Migrate from old auth bearer to new access token + + var deviceInfoService = Resolver.Resolve(); + var appIdService = Resolver.Resolve(); + + 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(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 + { + { "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(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() { - return ApiResult.Failed(System.Net.HttpStatusCode.RequestTimeout, + return ApiResult.Failed(HttpStatusCode.RequestTimeout, new ApiError { Message = "Not connected to the internet." }); } protected ApiResult HandledNotConnected() { - return ApiResult.Failed(System.Net.HttpStatusCode.RequestTimeout, + return ApiResult.Failed(HttpStatusCode.RequestTimeout, new ApiError { Message = "Not connected to the internet." }); } 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." }); } 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." }); } diff --git a/src/App/Repositories/CipherApiRepository.cs b/src/App/Repositories/CipherApiRepository.cs index cd4483681..946b6cec7 100644 --- a/src/App/Repositories/CipherApiRepository.cs +++ b/src/App/Repositories/CipherApiRepository.cs @@ -14,8 +14,9 @@ namespace Bit.App.Repositories { public CipherApiRepository( IConnectivity connectivity, - IHttpService httpService) - : base(connectivity, httpService) + IHttpService httpService, + ITokenService tokenService) + : base(connectivity, httpService, tokenService) { } protected override string ApiRoute => "ciphers"; @@ -27,6 +28,12 @@ namespace Bit.App.Repositories return HandledNotConnected(); } + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse.Succeeded) + { + return tokenStateResponse; + } + using(var client = HttpService.Client) { var requestMessage = new TokenHttpRequestMessage() @@ -61,6 +68,12 @@ namespace Bit.App.Repositories return HandledNotConnected>(); } + var tokenStateResponse = await HandleTokenStateAsync>(); + if(!tokenStateResponse.Succeeded) + { + return tokenStateResponse; + } + using(var client = HttpService.Client) { var requestMessage = new TokenHttpRequestMessage() @@ -95,6 +108,12 @@ namespace Bit.App.Repositories return HandledNotConnected(); } + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse.Succeeded) + { + return tokenStateResponse; + } + using(var client = HttpService.Client) { var requestMessage = new TokenHttpRequestMessage() diff --git a/src/App/Repositories/ConnectApiRepository.cs b/src/App/Repositories/ConnectApiRepository.cs index 69b586b9c..8b6769260 100644 --- a/src/App/Repositories/ConnectApiRepository.cs +++ b/src/App/Repositories/ConnectApiRepository.cs @@ -13,8 +13,9 @@ namespace Bit.App.Repositories { public ConnectApiRepository( IConnectivity connectivity, - IHttpService httpService) - : base(connectivity, httpService) + IHttpService httpService, + ITokenService tokenService) + : base(connectivity, httpService, tokenService) { } protected override string ApiRoute => "connect"; diff --git a/src/App/Repositories/DeviceApiRepository.cs b/src/App/Repositories/DeviceApiRepository.cs index 95c5a7031..5d99d721b 100644 --- a/src/App/Repositories/DeviceApiRepository.cs +++ b/src/App/Repositories/DeviceApiRepository.cs @@ -13,8 +13,9 @@ namespace Bit.App.Repositories { public DeviceApiRepository( IConnectivity connectivity, - IHttpService httpService) - : base(connectivity, httpService) + IHttpService httpService, + ITokenService tokenService) + : base(connectivity, httpService, tokenService) { } protected override string ApiRoute => "devices"; @@ -26,6 +27,12 @@ namespace Bit.App.Repositories return HandledNotConnected(); } + var tokenStateResponse = await HandleTokenStateAsync(); + if(!tokenStateResponse.Succeeded) + { + return tokenStateResponse; + } + using(var client = HttpService.Client) { var requestMessage = new TokenHttpRequestMessage(request) diff --git a/src/App/Repositories/FolderApiRepository.cs b/src/App/Repositories/FolderApiRepository.cs index 2bf528a04..79d99808d 100644 --- a/src/App/Repositories/FolderApiRepository.cs +++ b/src/App/Repositories/FolderApiRepository.cs @@ -14,8 +14,9 @@ namespace Bit.App.Repositories { public FolderApiRepository( IConnectivity connectivity, - IHttpService httpService) - : base(connectivity, httpService) + IHttpService httpService, + ITokenService tokenService) + : base(connectivity, httpService, tokenService) { } protected override string ApiRoute => "folders"; @@ -27,6 +28,12 @@ namespace Bit.App.Repositories return HandledNotConnected>(); } + var tokenStateResponse = await HandleTokenStateAsync>(); + if(!tokenStateResponse.Succeeded) + { + return tokenStateResponse; + } + using(var client = HttpService.Client) { var requestMessage = new TokenHttpRequestMessage() diff --git a/src/App/Repositories/LoginApiRepository.cs b/src/App/Repositories/LoginApiRepository.cs index 2fd35d7f2..6515d4208 100644 --- a/src/App/Repositories/LoginApiRepository.cs +++ b/src/App/Repositories/LoginApiRepository.cs @@ -13,8 +13,9 @@ namespace Bit.App.Repositories { public LoginApiRepository( IConnectivity connectivity, - IHttpService httpService) - : base(connectivity, httpService) + IHttpService httpService, + ITokenService tokenService) + : base(connectivity, httpService, tokenService) { } protected override string ApiRoute => "sites"; diff --git a/src/App/Services/TokenService.cs b/src/App/Services/TokenService.cs index 0cbc6df30..f55d90962 100644 --- a/src/App/Services/TokenService.cs +++ b/src/App/Services/TokenService.cs @@ -2,6 +2,7 @@ using Bit.App.Abstractions; using System.Text; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Bit.App.Services { @@ -67,22 +68,21 @@ namespace Bit.App.Services get { var decoded = DecodeToken(); - long exp = 0; - if(decoded?.exp != null || !long.TryParse(decoded.exp, out exp)) + if(decoded?["exp"] == null) { throw new InvalidOperationException("No exp in token."); } - return _epoc.AddSeconds(Convert.ToDouble(exp)); + return _epoc.AddSeconds(Convert.ToDouble(decoded["exp"].Value())); } } public bool TokenExpired => DateTime.UtcNow < TokenExpiration; public TimeSpan TokenTimeRemaining => TokenExpiration - DateTime.UtcNow; - public bool TokenNeedseRefresh => TokenTimeRemaining.TotalMinutes < 5; - public string TokenUserId => DecodeToken()?.sub; - public string TokenEmail => DecodeToken()?.email; - public string TokenName => DecodeToken()?.name; + public bool TokenNeedsRefresh => TokenTimeRemaining.TotalMinutes < 5; + public string TokenUserId => DecodeToken()?["sub"].Value(); + public string TokenEmail => DecodeToken()?["email"].Value(); + public string TokenName => DecodeToken()?["name"].Value(); public string RefreshToken { @@ -152,7 +152,7 @@ namespace Bit.App.Services } } - public dynamic DecodeToken() + public JObject DecodeToken() { if(_decodedToken != null) { @@ -176,7 +176,7 @@ namespace Bit.App.Services 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; }