mirror of
https://github.com/bitwarden/mobile.git
synced 2024-12-23 16:27:51 +01:00
auth apis and api helpers
This commit is contained in:
parent
579a7e0398
commit
567161d8f3
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<bool, Task> _logoutCallbackAsync;
|
||||
private string _deviceType;
|
||||
private bool _usingBaseUrl = false;
|
||||
|
||||
public ApiService(
|
||||
ITokenService tokenService,
|
||||
IPlatformUtilsService platformUtilsService)
|
||||
IPlatformUtilsService platformUtilsService,
|
||||
Func<bool, Task> 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<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")),
|
||||
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<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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user