Abstraction of authenticators. Reworked Gmail authentication.
This commit is contained in:
@@ -1,22 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Services;
|
||||
|
||||
namespace Wino.Core.Authenticators.Base
|
||||
{
|
||||
public abstract class BaseAuthenticator
|
||||
{
|
||||
public abstract MailProviderType ProviderType { get; }
|
||||
|
||||
protected ITokenService TokenService { get; }
|
||||
|
||||
protected BaseAuthenticator(ITokenService tokenService)
|
||||
{
|
||||
TokenService = tokenService;
|
||||
}
|
||||
|
||||
internal Task SaveTokenInternalAsync(MailAccount account, TokenInformation tokenInformation)
|
||||
=> TokenService.SaveTokenInformationAsync(account.Id, tokenInformation);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Services;
|
||||
|
||||
namespace Wino.Core.Authenticators.Base
|
||||
{
|
||||
public abstract class GmailAuthenticatorBase : BaseAuthenticator, IGmailAuthenticator
|
||||
{
|
||||
protected GmailAuthenticatorBase(ITokenService tokenService) : base(tokenService)
|
||||
{
|
||||
}
|
||||
|
||||
public abstract string ClientId { get; }
|
||||
public bool ProposeCopyAuthURL { get; set; }
|
||||
|
||||
public abstract Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken);
|
||||
|
||||
public abstract Task<TokenInformation> GetTokenAsync(MailAccount account);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Services;
|
||||
|
||||
namespace Wino.Core.Authenticators.Base
|
||||
{
|
||||
public abstract class OutlookAuthenticatorBase : BaseAuthenticator, IOutlookAuthenticator
|
||||
{
|
||||
protected OutlookAuthenticatorBase(ITokenService tokenService) : base(tokenService)
|
||||
{
|
||||
}
|
||||
|
||||
public abstract string ClientId { get; }
|
||||
|
||||
public abstract Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken);
|
||||
|
||||
public abstract Task<TokenInformation> GetTokenAsync(MailAccount account);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Authenticators.Base;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Services;
|
||||
|
||||
namespace Wino.Core.Authenticators.Calendar
|
||||
{
|
||||
public class OutlookAuthenticator : OutlookAuthenticatorBase
|
||||
{
|
||||
public OutlookAuthenticator(ITokenService tokenService) : base(tokenService)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ClientId => throw new NotImplementedException();
|
||||
|
||||
public override MailProviderType ProviderType => MailProviderType.Outlook;
|
||||
|
||||
public override Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<TokenInformation> GetTokenAsync(MailAccount account)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Authenticators.Base;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Authentication;
|
||||
using Wino.Core.Domain.Models.Authorization;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
namespace Wino.Core.Authenticators.Mail
|
||||
{
|
||||
public class GmailAuthenticator : GmailAuthenticatorBase
|
||||
{
|
||||
public override string ClientId { get; } = "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com";
|
||||
|
||||
private const string TokenEndpoint = "https://www.googleapis.com/oauth2/v4/token";
|
||||
private const string RefreshTokenEndpoint = "https://oauth2.googleapis.com/token";
|
||||
private const string UserInfoEndpoint = "https://gmail.googleapis.com/gmail/v1/users/me/profile";
|
||||
|
||||
public override MailProviderType ProviderType => MailProviderType.Gmail;
|
||||
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
|
||||
public GmailAuthenticator(ITokenService tokenService, INativeAppService nativeAppService) : base(tokenService)
|
||||
{
|
||||
_nativeAppService = nativeAppService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs tokenization code exchange and retrieves the actual Access - Refresh tokens from Google
|
||||
/// after redirect uri returns from browser.
|
||||
/// </summary>
|
||||
/// <param name="tokenizationRequest">Tokenization request.</param>
|
||||
/// <exception cref="GoogleAuthenticationException">In case of network or parsing related error.</exception>
|
||||
private async Task<TokenInformation> PerformCodeExchangeAsync(GoogleTokenizationRequest tokenizationRequest)
|
||||
{
|
||||
var uri = tokenizationRequest.BuildRequest();
|
||||
|
||||
var content = new StringContent(uri, Encoding.UTF8, "application/x-www-form-urlencoded");
|
||||
|
||||
var handler = new HttpClientHandler()
|
||||
{
|
||||
AllowAutoRedirect = true
|
||||
};
|
||||
|
||||
var client = new HttpClient(handler);
|
||||
|
||||
var response = await client.PostAsync(TokenEndpoint, content);
|
||||
string responseString = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new GoogleAuthenticationException(Translator.Exception_GoogleAuthorizationCodeExchangeFailed);
|
||||
|
||||
var parsed = JsonNode.Parse(responseString).AsObject();
|
||||
|
||||
if (parsed.ContainsKey("error"))
|
||||
throw new GoogleAuthenticationException(parsed["error"]["message"].GetValue<string>());
|
||||
|
||||
var accessToken = parsed["access_token"].GetValue<string>();
|
||||
var refreshToken = parsed["refresh_token"].GetValue<string>();
|
||||
var expiresIn = parsed["expires_in"].GetValue<long>();
|
||||
|
||||
var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn);
|
||||
|
||||
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
// Get basic user info for UserName.
|
||||
|
||||
var userinfoResponse = await client.GetAsync(UserInfoEndpoint);
|
||||
string userinfoResponseContent = await userinfoResponse.Content.ReadAsStringAsync();
|
||||
|
||||
var parsedUserInfo = JsonNode.Parse(userinfoResponseContent).AsObject();
|
||||
|
||||
if (parsedUserInfo.ContainsKey("error"))
|
||||
throw new GoogleAuthenticationException(parsedUserInfo["error"]["message"].GetValue<string>());
|
||||
|
||||
var username = parsedUserInfo["emailAddress"].GetValue<string>();
|
||||
|
||||
return new TokenInformation()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Address = username,
|
||||
AccessToken = accessToken,
|
||||
RefreshToken = refreshToken,
|
||||
ExpiresAt = expirationDate
|
||||
};
|
||||
}
|
||||
|
||||
public async override Task<TokenInformation> GetTokenAsync(MailAccount account)
|
||||
{
|
||||
var cachedToken = await TokenService.GetTokenInformationAsync(account.Id)
|
||||
?? throw new AuthenticationAttentionException(account);
|
||||
|
||||
if (cachedToken.IsExpired)
|
||||
{
|
||||
// Refresh token with new exchanges.
|
||||
// No need to check Username for account.
|
||||
|
||||
var refreshedTokenInfoBase = await RefreshTokenAsync(cachedToken.RefreshToken);
|
||||
|
||||
cachedToken.RefreshTokens(refreshedTokenInfoBase);
|
||||
|
||||
// Save new token and return.
|
||||
await SaveTokenInternalAsync(account, cachedToken);
|
||||
}
|
||||
|
||||
return cachedToken;
|
||||
}
|
||||
|
||||
|
||||
public async override Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken)
|
||||
{
|
||||
var authRequest = _nativeAppService.GetGoogleAuthorizationRequest();
|
||||
|
||||
var authorizationUri = authRequest.BuildRequest(ClientId);
|
||||
|
||||
Uri responseRedirectUri = null;
|
||||
|
||||
if (ProposeCopyAuthURL)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new CopyAuthURLRequested(authorizationUri));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
responseRedirectUri = await _nativeAppService.GetAuthorizationResponseUriAsync(this, authorizationUri);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw new AuthenticationException(Translator.Exception_AuthenticationCanceled);
|
||||
}
|
||||
|
||||
authRequest.ValidateAuthorizationCode(responseRedirectUri);
|
||||
|
||||
// Start tokenization.
|
||||
var tokenizationRequest = new GoogleTokenizationRequest(authRequest);
|
||||
var tokenInformation = await PerformCodeExchangeAsync(tokenizationRequest);
|
||||
|
||||
if (saveToken)
|
||||
{
|
||||
await SaveTokenInternalAsync(account, tokenInformation);
|
||||
}
|
||||
|
||||
return tokenInformation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internally exchanges refresh token with a new access token and returns new TokenInformation.
|
||||
/// </summary>
|
||||
/// <param name="refresh_token">Token to be used in refreshing.</param>
|
||||
/// <returns>New TokenInformationBase that has new tokens and expiration date without a username. This token is not saved to database after returned.</returns>
|
||||
private async Task<TokenInformationBase> RefreshTokenAsync(string refresh_token)
|
||||
{
|
||||
// TODO: This doesn't work.
|
||||
var refreshUri = string.Format("client_id={0}&refresh_token={1}&grant_type=refresh_token", ClientId, refresh_token);
|
||||
|
||||
//Uri.EscapeDataString(refreshUri);
|
||||
var content = new StringContent(refreshUri, Encoding.UTF8, "application/x-www-form-urlencoded");
|
||||
|
||||
var client = new HttpClient();
|
||||
|
||||
var response = await client.PostAsync(RefreshTokenEndpoint, content);
|
||||
|
||||
string responseString = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var parsed = JsonNode.Parse(responseString).AsObject();
|
||||
|
||||
// TODO: Error parsing is incorrect.
|
||||
if (parsed.ContainsKey("error"))
|
||||
throw new GoogleAuthenticationException(parsed["error_description"].GetValue<string>());
|
||||
|
||||
var accessToken = parsed["access_token"].GetValue<string>();
|
||||
|
||||
string activeRefreshToken = refresh_token;
|
||||
|
||||
// Refresh token might not be returned.
|
||||
// In this case older refresh token is still available for new refreshes.
|
||||
// Only change if provided.
|
||||
|
||||
if (parsed.ContainsKey("refresh_token"))
|
||||
{
|
||||
activeRefreshToken = parsed["refresh_token"].GetValue<string>();
|
||||
}
|
||||
|
||||
var expiresIn = parsed["expires_in"].GetValue<long>();
|
||||
var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn);
|
||||
|
||||
return new TokenInformationBase()
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
ExpiresAt = expirationDate,
|
||||
RefreshToken = activeRefreshToken
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Services;
|
||||
|
||||
namespace Wino.Core.Authenticators.Mail
|
||||
{
|
||||
public class Office365Authenticator : OutlookAuthenticator
|
||||
{
|
||||
public Office365Authenticator(ITokenService tokenService, INativeAppService nativeAppService, IApplicationConfiguration applicationConfiguration) : base(tokenService, nativeAppService, applicationConfiguration) { }
|
||||
|
||||
public override MailProviderType ProviderType => MailProviderType.Office365;
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Identity.Client;
|
||||
using Microsoft.Identity.Client.Broker;
|
||||
using Microsoft.Identity.Client.Extensions.Msal;
|
||||
using Wino.Core.Authenticators.Base;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.Services;
|
||||
|
||||
namespace Wino.Core.Authenticators.Mail
|
||||
{
|
||||
/// <summary>
|
||||
/// Authenticator for Outlook Mail provider.
|
||||
/// Token cache is managed by MSAL, not by Wino.
|
||||
/// </summary>
|
||||
public class OutlookAuthenticator : OutlookAuthenticatorBase
|
||||
{
|
||||
private const string TokenCacheFileName = "OutlookCache.bin";
|
||||
private bool isTokenCacheAttached = false;
|
||||
|
||||
// Outlook
|
||||
private const string Authority = "https://login.microsoftonline.com/common";
|
||||
|
||||
public override string ClientId { get; } = "b19c2035-d740-49ff-b297-de6ec561b208";
|
||||
|
||||
private readonly string[] MailScope =
|
||||
[
|
||||
"email",
|
||||
"mail.readwrite",
|
||||
"offline_access",
|
||||
"mail.send",
|
||||
"Mail.Send.Shared",
|
||||
"Mail.ReadWrite.Shared",
|
||||
"User.Read"
|
||||
];
|
||||
|
||||
public override MailProviderType ProviderType => MailProviderType.Outlook;
|
||||
|
||||
private readonly IPublicClientApplication _publicClientApplication;
|
||||
private readonly IApplicationConfiguration _applicationConfiguration;
|
||||
|
||||
public OutlookAuthenticator(ITokenService tokenService,
|
||||
INativeAppService nativeAppService,
|
||||
IApplicationConfiguration applicationConfiguration) : base(tokenService)
|
||||
{
|
||||
_applicationConfiguration = applicationConfiguration;
|
||||
|
||||
var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri();
|
||||
|
||||
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
|
||||
{
|
||||
Title = "Wino Mail",
|
||||
ListOperatingSystemAccounts = true,
|
||||
};
|
||||
|
||||
var outlookAppBuilder = PublicClientApplicationBuilder.Create(ClientId)
|
||||
.WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd)
|
||||
.WithBroker(options)
|
||||
.WithDefaultRedirectUri()
|
||||
.WithAuthority(Authority);
|
||||
|
||||
_publicClientApplication = outlookAppBuilder.Build();
|
||||
}
|
||||
|
||||
public override async Task<TokenInformation> GetTokenAsync(MailAccount account)
|
||||
{
|
||||
if (!isTokenCacheAttached)
|
||||
{
|
||||
var storageProperties = new StorageCreationPropertiesBuilder(TokenCacheFileName, _applicationConfiguration.PublisherSharedFolderPath).Build();
|
||||
var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties);
|
||||
msalcachehelper.RegisterCache(_publicClientApplication.UserTokenCache);
|
||||
|
||||
isTokenCacheAttached = true;
|
||||
}
|
||||
|
||||
var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(a => a.Username == account.Address);
|
||||
|
||||
// TODO: Handle it from the server.
|
||||
if (storedAccount == null) throw new AuthenticationAttentionException(account);
|
||||
|
||||
try
|
||||
{
|
||||
var authResult = await _publicClientApplication.AcquireTokenSilent(MailScope, storedAccount).ExecuteAsync();
|
||||
|
||||
return authResult.CreateTokenInformation() ?? throw new Exception("Failed to get Outlook token.");
|
||||
}
|
||||
catch (MsalUiRequiredException)
|
||||
{
|
||||
// Somehow MSAL is not able to refresh the token silently.
|
||||
// Force interactive login.
|
||||
return await GenerateTokenAsync(account, true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var authResult = await _publicClientApplication
|
||||
.AcquireTokenInteractive(MailScope)
|
||||
.ExecuteAsync();
|
||||
|
||||
var tokenInformation = authResult.CreateTokenInformation();
|
||||
|
||||
if (saveToken)
|
||||
{
|
||||
await SaveTokenInternalAsync(account, tokenInformation);
|
||||
}
|
||||
|
||||
return tokenInformation;
|
||||
}
|
||||
catch (MsalClientException msalClientException)
|
||||
{
|
||||
if (msalClientException.ErrorCode == "authentication_canceled" || msalClientException.ErrorCode == "access_denied")
|
||||
throw new AccountSetupCanceledException();
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
throw new AuthenticationException(Translator.Exception_UnknowErrorDuringAuthentication, new Exception(Translator.Exception_TokenGenerationFailed));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Serilog.Core;
|
||||
using Wino.Core.Authenticators.Mail;
|
||||
using Wino.Authentication;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Integration.Processors;
|
||||
using Wino.Core.Integration.Threading;
|
||||
@@ -28,7 +28,6 @@ namespace Wino.Core
|
||||
services.AddTransient<IImapChangeProcessor, ImapChangeProcessor>();
|
||||
services.AddTransient<IOutlookChangeProcessor, OutlookChangeProcessor>();
|
||||
|
||||
services.AddTransient<ITokenService, TokenService>();
|
||||
services.AddTransient<IFolderService, FolderService>();
|
||||
services.AddTransient<IMailService, MailService>();
|
||||
services.AddTransient<IAccountService, AccountService>();
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.Identity.Client;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
namespace Wino.Core.Extensions
|
||||
{
|
||||
public static class TokenizationExtensions
|
||||
{
|
||||
public static TokenInformation CreateTokenInformation(this AuthenticationResult clientBuilderResult)
|
||||
{
|
||||
// Plain access token info is not stored for Outlook in Wino's database.
|
||||
// Here we store UniqueId and Access Token in memory only to compare the UniqueId returned from MSAL auth result.
|
||||
|
||||
var tokenInfo = new TokenInformation()
|
||||
{
|
||||
Address = clientBuilderResult.Account.Username,
|
||||
Id = Guid.NewGuid(),
|
||||
UniqueId = clientBuilderResult.UniqueId,
|
||||
AccessToken = clientBuilderResult.AccessToken
|
||||
};
|
||||
|
||||
return tokenInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,29 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Apis.Http;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.Http
|
||||
{
|
||||
internal class GmailClientMessageHandler : ConfigurableMessageHandler
|
||||
{
|
||||
public Func<Task<TokenInformation>> TokenRetrieveDelegate { get; }
|
||||
private readonly IGmailAuthenticator _gmailAuthenticator;
|
||||
private readonly MailAccount _mailAccount;
|
||||
|
||||
public GmailClientMessageHandler(Func<Task<TokenInformation>> tokenRetrieveDelegate) : base(new HttpClientHandler())
|
||||
public GmailClientMessageHandler(IGmailAuthenticator gmailAuthenticator, MailAccount mailAccount) : base(new HttpClientHandler())
|
||||
{
|
||||
TokenRetrieveDelegate = tokenRetrieveDelegate;
|
||||
_gmailAuthenticator = gmailAuthenticator;
|
||||
_mailAccount = mailAccount;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var tokenizationTask = TokenRetrieveDelegate.Invoke();
|
||||
var tokenInformation = await tokenizationTask;
|
||||
// This call here will automatically trigger Google Auth's interactive login if the token is not found.
|
||||
// or refresh the token based on the FileDataStore.
|
||||
|
||||
var tokenInformation = await _gmailAuthenticator.GetTokenInformationAsync(_mailAccount);
|
||||
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenInformation.AccessToken);
|
||||
|
||||
|
||||
@@ -22,12 +22,12 @@ namespace Wino.Core.Http
|
||||
public AllowedHostsValidator AllowedHostsValidator { get; }
|
||||
|
||||
public async Task<string> GetAuthorizationTokenAsync(Uri uri,
|
||||
Dictionary<string, object> additionalAuthenticationContext = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
Dictionary<string, object> additionalAuthenticationContext = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var token = await _authenticator.GetTokenAsync(_account).ConfigureAwait(false);
|
||||
var tokenInfo = await _authenticator.GetTokenInformationAsync(_account);
|
||||
|
||||
return token?.AccessToken;
|
||||
return tokenInfo.AccessToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,8 +206,9 @@ namespace Wino.Core.Services
|
||||
var authenticator = _authenticationProvider.GetAuthenticator(account.ProviderType);
|
||||
|
||||
// This will re-generate token.
|
||||
var token = await authenticator.GenerateTokenAsync(account, true);
|
||||
var token = await authenticator.GenerateTokenInformationAsync(account);
|
||||
|
||||
// TODO: Rest?
|
||||
Guard.IsNotNull(token);
|
||||
}
|
||||
|
||||
@@ -267,10 +268,10 @@ namespace Wino.Core.Services
|
||||
public async Task DeleteAccountAsync(MailAccount account)
|
||||
{
|
||||
// TODO: Delete mime messages and attachments.
|
||||
// TODO: Delete token cache by underlying provider.
|
||||
|
||||
await Connection.ExecuteAsync("DELETE FROM MailCopy WHERE Id IN(SELECT Id FROM MailCopy WHERE FolderId IN (SELECT Id from MailItemFolder WHERE MailAccountId == ?))", account.Id);
|
||||
|
||||
await Connection.Table<TokenInformation>().Where(a => a.AccountId == account.Id).DeleteAsync();
|
||||
await Connection.Table<MailItemFolder>().DeleteAsync(a => a.MailAccountId == account.Id);
|
||||
await Connection.Table<AccountSignature>().DeleteAsync(a => a.MailAccountId == account.Id);
|
||||
await Connection.Table<MailAccountAlias>().DeleteAsync(a => a.AccountId == account.Id);
|
||||
@@ -333,6 +334,10 @@ namespace Wino.Core.Services
|
||||
account.SenderName = profileInformation.SenderName;
|
||||
account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
|
||||
|
||||
if (string.IsNullOrEmpty(account.Address))
|
||||
{
|
||||
account.Address = profileInformation.AccountAddress;
|
||||
}
|
||||
// Forcefully add or update a contact data with the provided information.
|
||||
|
||||
var accountContact = new AccountContact()
|
||||
@@ -469,7 +474,7 @@ namespace Wino.Core.Services
|
||||
await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task CreateAccountAsync(MailAccount account, TokenInformation tokenInformation, CustomServerInformation customServerInformation)
|
||||
public async Task CreateAccountAsync(MailAccount account, CustomServerInformation customServerInformation)
|
||||
{
|
||||
Guard.IsNotNull(account);
|
||||
|
||||
@@ -518,12 +523,6 @@ namespace Wino.Core.Services
|
||||
|
||||
if (customServerInformation != null)
|
||||
await Connection.InsertAsync(customServerInformation);
|
||||
|
||||
// Outlook token cache is managed by MSAL.
|
||||
// Don't save it to database.
|
||||
|
||||
if (tokenInformation != null && (account.ProviderType != MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365))
|
||||
await Connection.InsertAsync(tokenInformation);
|
||||
}
|
||||
|
||||
public async Task<string> UpdateSynchronizationIdentifierAsync(Guid accountId, string newIdentifier)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using Wino.Core.Authenticators.Mail;
|
||||
using Wino.Authentication;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
@@ -10,14 +10,16 @@ namespace Wino.Core.Services
|
||||
public class AuthenticationProvider : IAuthenticationProvider
|
||||
{
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly IApplicationConfiguration _applicationConfiguration;
|
||||
private readonly IAuthenticatorConfig _authenticatorConfig;
|
||||
|
||||
public AuthenticationProvider(INativeAppService nativeAppService, ITokenService tokenService, IApplicationConfiguration applicationConfiguration)
|
||||
public AuthenticationProvider(INativeAppService nativeAppService,
|
||||
IApplicationConfiguration applicationConfiguration,
|
||||
IAuthenticatorConfig authenticatorConfig)
|
||||
{
|
||||
_nativeAppService = nativeAppService;
|
||||
_tokenService = tokenService;
|
||||
_applicationConfiguration = applicationConfiguration;
|
||||
_authenticatorConfig = authenticatorConfig;
|
||||
}
|
||||
|
||||
public IAuthenticator GetAuthenticator(MailProviderType providerType)
|
||||
@@ -25,9 +27,9 @@ namespace Wino.Core.Services
|
||||
// TODO: Move DI
|
||||
return providerType switch
|
||||
{
|
||||
MailProviderType.Outlook => new OutlookAuthenticator(_tokenService, _nativeAppService, _applicationConfiguration),
|
||||
MailProviderType.Office365 => new Office365Authenticator(_tokenService, _nativeAppService, _applicationConfiguration),
|
||||
MailProviderType.Gmail => new GmailAuthenticator(_tokenService, _nativeAppService),
|
||||
MailProviderType.Outlook => new OutlookAuthenticator(_nativeAppService, _applicationConfiguration, _authenticatorConfig),
|
||||
MailProviderType.Office365 => new Office365Authenticator(_nativeAppService, _applicationConfiguration, _authenticatorConfig),
|
||||
MailProviderType.Gmail => new GmailAuthenticator(_authenticatorConfig),
|
||||
_ => throw new ArgumentException(Translator.Exception_UnsupportedProvider),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SQLite;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
@@ -35,16 +34,7 @@ namespace Wino.Core.Services
|
||||
var publisherCacheFolder = _folderConfiguration.PublisherSharedFolderPath;
|
||||
var databaseFileName = Path.Combine(publisherCacheFolder, DatabaseName);
|
||||
|
||||
Connection = new SQLiteAsyncConnection(databaseFileName)
|
||||
{
|
||||
// Enable for debugging sqlite.
|
||||
Trace = true,
|
||||
Tracer = new Action<string>((t) =>
|
||||
{
|
||||
// Debug.WriteLine(t);
|
||||
// Log.Debug(t);
|
||||
})
|
||||
};
|
||||
Connection = new SQLiteAsyncConnection(databaseFileName);
|
||||
|
||||
await CreateTablesAsync();
|
||||
|
||||
@@ -57,7 +47,6 @@ namespace Wino.Core.Services
|
||||
typeof(MailCopy),
|
||||
typeof(MailItemFolder),
|
||||
typeof(MailAccount),
|
||||
typeof(TokenInformation),
|
||||
typeof(AccountContact),
|
||||
typeof(CustomServerInformation),
|
||||
typeof(AccountSignature),
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
|
||||
namespace Wino.Core.Services
|
||||
{
|
||||
public interface ITokenService
|
||||
{
|
||||
Task<TokenInformation> GetTokenInformationAsync(Guid accountId);
|
||||
Task SaveTokenInformationAsync(Guid accountId, TokenInformation tokenInformation);
|
||||
}
|
||||
|
||||
public class TokenService : BaseDatabaseService, ITokenService
|
||||
{
|
||||
public TokenService(IDatabaseService databaseService) : base(databaseService) { }
|
||||
|
||||
public Task<TokenInformation> GetTokenInformationAsync(Guid accountId)
|
||||
=> Connection.Table<TokenInformation>().FirstOrDefaultAsync(a => a.AccountId == accountId);
|
||||
|
||||
public async Task SaveTokenInformationAsync(Guid accountId, TokenInformation tokenInformation)
|
||||
{
|
||||
// Delete all tokens for this account.
|
||||
await Connection.Table<TokenInformation>().DeleteAsync(a => a.AccountId == accountId);
|
||||
|
||||
// Save new token info to the account.
|
||||
tokenInformation.AccountId = accountId;
|
||||
|
||||
await Connection.InsertOrReplaceAsync(tokenInformation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,11 @@ namespace Wino.Core.Synchronizers
|
||||
{
|
||||
Account.SenderName = profileInformation.SenderName;
|
||||
Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
|
||||
|
||||
if (!string.IsNullOrEmpty(profileInformation.AccountAddress))
|
||||
{
|
||||
Account.Address = profileInformation.AccountAddress;
|
||||
}
|
||||
}
|
||||
|
||||
return profileInformation;
|
||||
|
||||
@@ -46,15 +46,15 @@ namespace Wino.Core.Synchronizers.Mail
|
||||
private readonly GmailService _gmailService;
|
||||
private readonly PeopleServiceService _peopleService;
|
||||
|
||||
private readonly IAuthenticator _authenticator;
|
||||
private readonly IGmailAuthenticator _authenticator;
|
||||
private readonly IGmailChangeProcessor _gmailChangeProcessor;
|
||||
private readonly ILogger _logger = Log.ForContext<GmailSynchronizer>();
|
||||
|
||||
public GmailSynchronizer(MailAccount account,
|
||||
IAuthenticator authenticator,
|
||||
IGmailAuthenticator authenticator,
|
||||
IGmailChangeProcessor gmailChangeProcessor) : base(account)
|
||||
{
|
||||
var messageHandler = new GmailClientMessageHandler(() => _authenticator.GetTokenAsync(Account));
|
||||
var messageHandler = new GmailClientMessageHandler(authenticator, account);
|
||||
|
||||
var initializer = new BaseClientService.Initializer()
|
||||
{
|
||||
@@ -77,8 +77,12 @@ namespace Wino.Core.Synchronizers.Mail
|
||||
var profileRequest = _peopleService.People.Get("people/me");
|
||||
profileRequest.PersonFields = "names,photos";
|
||||
|
||||
string senderName = string.Empty, base64ProfilePicture = string.Empty;
|
||||
string senderName = string.Empty, base64ProfilePicture = string.Empty, address = string.Empty;
|
||||
|
||||
var gmailUserData = _gmailService.Users.GetProfile("me");
|
||||
var gmailProfile = await gmailUserData.ExecuteAsync();
|
||||
|
||||
address = gmailProfile.EmailAddress;
|
||||
var userProfile = await profileRequest.ExecuteAsync();
|
||||
|
||||
senderName = userProfile.Names?.FirstOrDefault()?.DisplayName ?? Account.SenderName;
|
||||
@@ -90,7 +94,7 @@ namespace Wino.Core.Synchronizers.Mail
|
||||
base64ProfilePicture = await GetProfilePictureBase64EncodedAsync(profilePicture).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new ProfileInformation(senderName, base64ProfilePicture);
|
||||
return new ProfileInformation(senderName, base64ProfilePicture, address);
|
||||
}
|
||||
|
||||
protected override async Task SynchronizeAliasesAsync()
|
||||
|
||||
@@ -533,20 +533,20 @@ namespace Wino.Core.Synchronizers.Mail
|
||||
/// <summary>
|
||||
/// Get the user's display name.
|
||||
/// </summary>
|
||||
/// <returns>Display name of the user.</returns>
|
||||
private async Task<string> GetSenderNameAsync()
|
||||
/// <returns>Display name and address of the user.</returns>
|
||||
private async Task<Tuple<string, string>> GetDisplayNameAndAddressAsync()
|
||||
{
|
||||
var userInfo = await _graphClient.Me.GetAsync();
|
||||
|
||||
return userInfo.DisplayName;
|
||||
return new Tuple<string, string>(userInfo.DisplayName, userInfo.Mail);
|
||||
}
|
||||
|
||||
public override async Task<ProfileInformation> GetProfileInformationAsync()
|
||||
{
|
||||
var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false);
|
||||
var senderName = await GetSenderNameAsync().ConfigureAwait(false);
|
||||
var displayNameAndAddress = await GetDisplayNameAndAddressAsync().ConfigureAwait(false);
|
||||
|
||||
return new ProfileInformation(senderName, profilePictureData);
|
||||
return new ProfileInformation(displayNameAndAddress.Item1, profilePictureData, displayNameAndAddress.Item2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wino.Authentication\Wino.Authentication.csproj" />
|
||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user