From 7fad15524f612cb18c31555ae482aeba154ee0f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 20 Nov 2024 01:45:48 +0100 Subject: [PATCH] Abstraction of authenticators. Reworked Gmail authentication. --- Wino.Authentication/BaseAuthenticator.cs | 17 ++ Wino.Authentication/GmailAuthenticator.cs | 53 +++++ Wino.Authentication/Office365Authenticator.cs | 16 ++ .../OutlookAuthenticator.cs | 69 +++--- .../Wino.Authentication.csproj | 24 +++ .../Entities/Shared/TokenInformation.cs | 30 --- .../Interfaces/IAccountService.cs | 2 +- Wino.Core.Domain/Interfaces/IAuthenticator.cs | 41 ++-- .../Interfaces/IAuthenticatorConfig.cs | 10 + .../Interfaces/IAuthenticatorTypes.cs | 1 + .../Interfaces/INativeAppService.cs | 14 -- .../Models/Accounts/ProfileInformation.cs | 3 +- .../Authentication/TokenInformationBase.cs | 20 -- .../Authentication/TokenInformationEx.cs | 11 + Wino.Core.UWP/Services/NativeAppService.cs | 24 --- Wino.Core.UWP/Styles/DataTemplates.xaml | 2 +- .../Authenticators/Base/BaseAuthenticator.cs | 22 -- .../Base/GmailAuthenticatorBase.cs | 21 -- .../Base/OutlookAuthenticatorBase.cs | 20 -- .../Calendar/OutlookAuthenticator.cs | 30 --- .../Authenticators/Mail/GmailAuthenticator.cs | 204 ------------------ .../Mail/Office365Authenticator.cs | 13 -- Wino.Core/CoreContainerSetup.cs | 3 +- .../Extensions/TokenizationExtensions.cs | 25 --- Wino.Core/Http/GmailClientMessageHandler.cs | 18 +- Wino.Core/Http/MicrosoftTokenProvider.cs | 8 +- Wino.Core/Services/AccountService.cs | 17 +- Wino.Core/Services/AuthenticationProvider.cs | 16 +- Wino.Core/Services/DatabaseService.cs | 15 +- Wino.Core/Services/TokenService.cs | 31 --- .../Synchronizers/BaseMailSynchronizer.cs | 5 + .../Synchronizers/Mail/GmailSynchronizer.cs | 14 +- .../Synchronizers/Mail/OutlookSynchronizer.cs | 10 +- Wino.Core/Wino.Core.csproj | 1 + .../AccountManagementViewModel.cs | 24 ++- Wino.Mail/App.xaml.cs | 2 +- Wino.Mail/Package.appxmanifest | 2 +- .../MailAuthenticatorConfiguration.cs | 29 +++ Wino.Mail/Wino.Mail.csproj | 10 +- Wino.Server/App.xaml.cs | 15 ++ .../MessageHandlers/AuthenticationHandler.cs | 27 ++- Wino.Server/ServerViewModel.cs | 15 -- Wino.Server/Wino.Server.csproj | 2 + Wino.sln | 23 ++ 44 files changed, 354 insertions(+), 605 deletions(-) create mode 100644 Wino.Authentication/BaseAuthenticator.cs create mode 100644 Wino.Authentication/GmailAuthenticator.cs create mode 100644 Wino.Authentication/Office365Authenticator.cs rename {Wino.Core/Authenticators/Mail => Wino.Authentication}/OutlookAuthenticator.cs (64%) create mode 100644 Wino.Authentication/Wino.Authentication.csproj delete mode 100644 Wino.Core.Domain/Entities/Shared/TokenInformation.cs create mode 100644 Wino.Core.Domain/Interfaces/IAuthenticatorConfig.cs delete mode 100644 Wino.Core.Domain/Models/Authentication/TokenInformationBase.cs create mode 100644 Wino.Core.Domain/Models/Authentication/TokenInformationEx.cs delete mode 100644 Wino.Core/Authenticators/Base/BaseAuthenticator.cs delete mode 100644 Wino.Core/Authenticators/Base/GmailAuthenticatorBase.cs delete mode 100644 Wino.Core/Authenticators/Base/OutlookAuthenticatorBase.cs delete mode 100644 Wino.Core/Authenticators/Calendar/OutlookAuthenticator.cs delete mode 100644 Wino.Core/Authenticators/Mail/GmailAuthenticator.cs delete mode 100644 Wino.Core/Authenticators/Mail/Office365Authenticator.cs delete mode 100644 Wino.Core/Extensions/TokenizationExtensions.cs delete mode 100644 Wino.Core/Services/TokenService.cs create mode 100644 Wino.Mail/Services/MailAuthenticatorConfiguration.cs diff --git a/Wino.Authentication/BaseAuthenticator.cs b/Wino.Authentication/BaseAuthenticator.cs new file mode 100644 index 00000000..4e3664df --- /dev/null +++ b/Wino.Authentication/BaseAuthenticator.cs @@ -0,0 +1,17 @@ +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Authentication +{ + public abstract class BaseAuthenticator + { + public abstract MailProviderType ProviderType { get; } + protected IAuthenticatorConfig AuthenticatorConfig { get; } + + protected BaseAuthenticator(IAuthenticatorConfig authenticatorConfig) + { + + AuthenticatorConfig = authenticatorConfig; + } + } +} diff --git a/Wino.Authentication/GmailAuthenticator.cs b/Wino.Authentication/GmailAuthenticator.cs new file mode 100644 index 00000000..54445e3f --- /dev/null +++ b/Wino.Authentication/GmailAuthenticator.cs @@ -0,0 +1,53 @@ +using System.Threading; +using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Util.Store; +using Wino.Core.Domain.Entities.Shared; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Authentication; + +namespace Wino.Authentication +{ + public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator + { + private const string FileDataStoreFolder = "WinoGmailStore"; + + public GmailAuthenticator(IAuthenticatorConfig authConfig) : base(authConfig) + { + } + + public string ClientId => AuthenticatorConfig.GmailAuthenticatorClientId; + public bool ProposeCopyAuthURL { get; set; } + + public override MailProviderType ProviderType => MailProviderType.Gmail; + + /// + /// Generates the token information for the given account. + /// For gmail, interactivity is automatically handled when you get the token. + /// + /// Account to get token for. + public Task GenerateTokenInformationAsync(MailAccount account) + => GetTokenInformationAsync(account); + + public async Task GetTokenInformationAsync(MailAccount account) + { + var userCredential = await GetGoogleUserCredentialAsync(account); + + if (userCredential.Token.IsStale) + { + await userCredential.RefreshTokenAsync(CancellationToken.None); + } + + return new TokenInformationEx(userCredential.Token.AccessToken, account.Address); + } + + private Task GetGoogleUserCredentialAsync(MailAccount account) + { + return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets() + { + ClientId = ClientId + }, AuthenticatorConfig.GmailScope, account.Id.ToString(), CancellationToken.None, new FileDataStore(FileDataStoreFolder)); + } + } +} diff --git a/Wino.Authentication/Office365Authenticator.cs b/Wino.Authentication/Office365Authenticator.cs new file mode 100644 index 00000000..7bb1017a --- /dev/null +++ b/Wino.Authentication/Office365Authenticator.cs @@ -0,0 +1,16 @@ +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Authentication +{ + public class Office365Authenticator : OutlookAuthenticator + { + public Office365Authenticator(INativeAppService nativeAppService, + IApplicationConfiguration applicationConfiguration, + IAuthenticatorConfig authenticatorConfig) : base(nativeAppService, applicationConfiguration, authenticatorConfig) + { + } + + public override MailProviderType ProviderType => MailProviderType.Office365; + } +} diff --git a/Wino.Core/Authenticators/Mail/OutlookAuthenticator.cs b/Wino.Authentication/OutlookAuthenticator.cs similarity index 64% rename from Wino.Core/Authenticators/Mail/OutlookAuthenticator.cs rename to Wino.Authentication/OutlookAuthenticator.cs index e4b8fe9d..8bdcfb27 100644 --- a/Wino.Core/Authenticators/Mail/OutlookAuthenticator.cs +++ b/Wino.Authentication/OutlookAuthenticator.cs @@ -4,22 +4,16 @@ 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; +using Wino.Core.Domain.Models.Authentication; -namespace Wino.Core.Authenticators.Mail +namespace Wino.Authentication { - /// - /// Authenticator for Outlook Mail provider. - /// Token cache is managed by MSAL, not by Wino. - /// - public class OutlookAuthenticator : OutlookAuthenticatorBase + public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator { private const string TokenCacheFileName = "OutlookCache.bin"; private bool isTokenCacheAttached = false; @@ -27,27 +21,14 @@ namespace Wino.Core.Authenticators.Mail // 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) + public OutlookAuthenticator(INativeAppService nativeAppService, + IApplicationConfiguration applicationConfiguration, + IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig) { _applicationConfiguration = applicationConfiguration; @@ -59,7 +40,7 @@ namespace Wino.Core.Authenticators.Mail ListOperatingSystemAccounts = true, }; - var outlookAppBuilder = PublicClientApplicationBuilder.Create(ClientId) + var outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId) .WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd) .WithBroker(options) .WithDefaultRedirectUri() @@ -68,7 +49,9 @@ namespace Wino.Core.Authenticators.Mail _publicClientApplication = outlookAppBuilder.Build(); } - public override async Task GetTokenAsync(MailAccount account) + public string[] Scope => AuthenticatorConfig.OutlookScope; + + private async Task EnsureTokenCacheAttachedAsync() { if (!isTokenCacheAttached) { @@ -78,23 +61,29 @@ namespace Wino.Core.Authenticators.Mail isTokenCacheAttached = true; } + } + + public async Task GetTokenInformationAsync(MailAccount account) + { + await EnsureTokenCacheAttachedAsync(); var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(a => a.Username == account.Address); - // TODO: Handle it from the server. - if (storedAccount == null) throw new AuthenticationAttentionException(account); + if (storedAccount == null) + return await GenerateTokenInformationAsync(account); try { - var authResult = await _publicClientApplication.AcquireTokenSilent(MailScope, storedAccount).ExecuteAsync(); + var authResult = await _publicClientApplication.AcquireTokenSilent(Scope, storedAccount).ExecuteAsync(); - return authResult.CreateTokenInformation() ?? throw new Exception("Failed to get Outlook token."); + return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username); } catch (MsalUiRequiredException) { // Somehow MSAL is not able to refresh the token silently. // Force interactive login. - return await GenerateTokenAsync(account, true); + + return await GenerateTokenInformationAsync(account); } catch (Exception) { @@ -102,22 +91,26 @@ namespace Wino.Core.Authenticators.Mail } } - public override async Task GenerateTokenAsync(MailAccount account, bool saveToken) + public async Task GenerateTokenInformationAsync(MailAccount account) { try { + await EnsureTokenCacheAttachedAsync(); + var authResult = await _publicClientApplication - .AcquireTokenInteractive(MailScope) + .AcquireTokenInteractive(Scope) .ExecuteAsync(); - var tokenInformation = authResult.CreateTokenInformation(); + // If the account is null, it means it's the initial creation of it. + // If not, make sure the authenticated user address matches the username. + // When people refresh their token, accounts must match. - if (saveToken) + if (account?.Address != null && !account.Address.Equals(authResult.Account.Username, StringComparison.OrdinalIgnoreCase)) { - await SaveTokenInternalAsync(account, tokenInformation); + throw new AuthenticationException("Authenticated address does not match with your account address."); } - return tokenInformation; + return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username); } catch (MsalClientException msalClientException) { diff --git a/Wino.Authentication/Wino.Authentication.csproj b/Wino.Authentication/Wino.Authentication.csproj new file mode 100644 index 00000000..09b5a3d2 --- /dev/null +++ b/Wino.Authentication/Wino.Authentication.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + Wino.Authentication + Debug;Release + 12 + AnyCPU;x64;x86 + + + + + + + + + + + + + + + + diff --git a/Wino.Core.Domain/Entities/Shared/TokenInformation.cs b/Wino.Core.Domain/Entities/Shared/TokenInformation.cs deleted file mode 100644 index e3204f9c..00000000 --- a/Wino.Core.Domain/Entities/Shared/TokenInformation.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using SQLite; -using Wino.Core.Domain.Models.Authentication; - -namespace Wino.Core.Domain.Entities.Shared -{ - public class TokenInformation : TokenInformationBase - { - [PrimaryKey] - public Guid Id { get; set; } - - public Guid AccountId { get; set; } - - /// - /// Unique object storage for authenticators if needed. - /// - public string UniqueId { get; set; } - public string Address { get; set; } - - public void RefreshTokens(TokenInformationBase tokenInformationBase) - { - if (tokenInformationBase == null) - throw new ArgumentNullException(nameof(tokenInformationBase)); - - AccessToken = tokenInformationBase.AccessToken; - RefreshToken = tokenInformationBase.RefreshToken; - ExpiresAt = tokenInformationBase.ExpiresAt; - } - } -} diff --git a/Wino.Core.Domain/Interfaces/IAccountService.cs b/Wino.Core.Domain/Interfaces/IAccountService.cs index 5eb83845..aac9b19b 100644 --- a/Wino.Core.Domain/Interfaces/IAccountService.cs +++ b/Wino.Core.Domain/Interfaces/IAccountService.cs @@ -48,7 +48,7 @@ namespace Wino.Core.Domain.Interfaces /// Creates new account with the given server information if any. /// Also sets the account as Startup account if there are no accounts. /// - Task CreateAccountAsync(MailAccount account, TokenInformation tokenInformation, CustomServerInformation customServerInformation); + Task CreateAccountAsync(MailAccount account, CustomServerInformation customServerInformation); /// /// Fixed authentication errors for account by forcing interactive login. diff --git a/Wino.Core.Domain/Interfaces/IAuthenticator.cs b/Wino.Core.Domain/Interfaces/IAuthenticator.cs index 93dad514..86d2f8b4 100644 --- a/Wino.Core.Domain/Interfaces/IAuthenticator.cs +++ b/Wino.Core.Domain/Interfaces/IAuthenticator.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Authentication; namespace Wino.Core.Domain.Interfaces { @@ -11,26 +12,28 @@ namespace Wino.Core.Domain.Interfaces /// MailProviderType ProviderType { get; } - /// - /// Gets the token from the cache if exists. - /// If the token is expired, tries to refresh. - /// This can throw AuthenticationAttentionException if silent refresh fails. - /// - /// Account to get token for. - /// Valid token info to be used in integrators. - Task GetTokenAsync(MailAccount account); + Task GetTokenInformationAsync(MailAccount account); - /// - /// Initial creation of token. Requires user interaction. - /// This will cache the token but still returns for account creation - /// since account address is required. - /// - /// Freshly created TokenInformation.. - Task GenerateTokenAsync(MailAccount account, bool saveToken); + Task GenerateTokenInformationAsync(MailAccount account); - /// - /// ClientId in case of needed for authorization/authentication. - /// - string ClientId { get; } + ///// + ///// Gets the token for the given account from the cache. + ///// Forces interactive login if the token is not found. + ///// + ///// Account to get access token for. + ///// Access token + //Task GetTokenAsync(MailAccount account); + + ///// + ///// Forces an interactive login to get the token for the given account. + ///// + ///// Account to get access token for. + ///// Access token + //// Task GenerateTokenAsync(MailAccount account); + + ///// + ///// ClientId in case of needed for authorization/authentication. + ///// + //string ClientId { get; } } } diff --git a/Wino.Core.Domain/Interfaces/IAuthenticatorConfig.cs b/Wino.Core.Domain/Interfaces/IAuthenticatorConfig.cs new file mode 100644 index 00000000..435e4489 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IAuthenticatorConfig.cs @@ -0,0 +1,10 @@ +namespace Wino.Core.Domain.Interfaces +{ + public interface IAuthenticatorConfig + { + string OutlookAuthenticatorClientId { get; } + string[] OutlookScope { get; } + string GmailAuthenticatorClientId { get; } + string[] GmailScope { get; } + } +} diff --git a/Wino.Core.Domain/Interfaces/IAuthenticatorTypes.cs b/Wino.Core.Domain/Interfaces/IAuthenticatorTypes.cs index cb8a12e8..ef842706 100644 --- a/Wino.Core.Domain/Interfaces/IAuthenticatorTypes.cs +++ b/Wino.Core.Domain/Interfaces/IAuthenticatorTypes.cs @@ -5,5 +5,6 @@ { bool ProposeCopyAuthURL { get; set; } } + public interface IImapAuthenticator : IAuthenticator { } } diff --git a/Wino.Core.Domain/Interfaces/INativeAppService.cs b/Wino.Core.Domain/Interfaces/INativeAppService.cs index ff933592..19eca7da 100644 --- a/Wino.Core.Domain/Interfaces/INativeAppService.cs +++ b/Wino.Core.Domain/Interfaces/INativeAppService.cs @@ -1,7 +1,5 @@ using System; -using System.Threading; using System.Threading.Tasks; -using Wino.Core.Domain.Models.Authorization; namespace Wino.Core.Domain.Interfaces { @@ -13,13 +11,6 @@ namespace Wino.Core.Domain.Interfaces Task LaunchFileAsync(string filePath); Task LaunchUriAsync(Uri uri); - /// - /// Launches the default browser with the specified uri and waits for protocol activation to finish. - /// - /// - /// Response callback from the browser. - Task GetAuthorizationResponseUriAsync(IAuthenticator authenticator, string authorizationUri, CancellationToken cancellationToken = default); - /// /// Finalizes GetAuthorizationResponseUriAsync for current IAuthenticator. /// @@ -32,11 +23,6 @@ namespace Wino.Core.Domain.Interfaces Task PinAppToTaskbarAsync(); - /// - /// Some cryptographic shit is needed for requesting Google authentication in UWP. - /// - GoogleAuthorizationRequest GetGoogleAuthorizationRequest(); - /// /// Gets or sets the function that returns a pointer for main window hwnd for UWP. /// This is used to display WAM broker dialog on running UWP app called by a windowless server code. diff --git a/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs b/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs index b9fb5677..c0d38911 100644 --- a/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs +++ b/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs @@ -5,5 +5,6 @@ /// /// Display sender name for the account. /// Base 64 encoded profile picture data of the account. Thumbnail size. - public record ProfileInformation(string SenderName, string Base64ProfilePictureData); + /// Address of the profile. + public record ProfileInformation(string SenderName, string Base64ProfilePictureData, string AccountAddress); } diff --git a/Wino.Core.Domain/Models/Authentication/TokenInformationBase.cs b/Wino.Core.Domain/Models/Authentication/TokenInformationBase.cs deleted file mode 100644 index 27f69abf..00000000 --- a/Wino.Core.Domain/Models/Authentication/TokenInformationBase.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Wino.Core.Domain.Models.Authentication -{ - public class TokenInformationBase - { - public string AccessToken { get; set; } - public string RefreshToken { get; set; } - - /// - /// UTC date for token expiration. - /// - public DateTime ExpiresAt { get; set; } - - /// - /// Gets the value indicating whether the token is expired or not. - /// - public bool IsExpired => DateTime.UtcNow >= ExpiresAt; - } -} diff --git a/Wino.Core.Domain/Models/Authentication/TokenInformationEx.cs b/Wino.Core.Domain/Models/Authentication/TokenInformationEx.cs new file mode 100644 index 00000000..270d9af7 --- /dev/null +++ b/Wino.Core.Domain/Models/Authentication/TokenInformationEx.cs @@ -0,0 +1,11 @@ +namespace Wino.Core.Domain.Models.Authentication +{ + /// + /// Previously known as TokenInformation. + /// We used to store this model in the database. + /// Now we store it in the memory. + /// + /// Access token/ + /// Address of the authenticated user. + public record TokenInformationEx(string AccessToken, string AccountAddress); +} diff --git a/Wino.Core.UWP/Services/NativeAppService.cs b/Wino.Core.UWP/Services/NativeAppService.cs index 00e59663..9e0e977b 100644 --- a/Wino.Core.UWP/Services/NativeAppService.cs +++ b/Wino.Core.UWP/Services/NativeAppService.cs @@ -1,7 +1,5 @@ using System; -using System.Threading; using System.Threading.Tasks; -using Nito.AsyncEx; using Windows.ApplicationModel; using Windows.Foundation.Metadata; using Windows.Security.Authentication.Web; @@ -11,8 +9,6 @@ using Windows.Storage; using Windows.Storage.Streams; using Windows.System; using Windows.UI.Shell; -using Wino.Core.Domain; -using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Authorization; @@ -156,26 +152,6 @@ namespace Wino.Services await taskbarManager.RequestPinCurrentAppAsync(); } - public async Task GetAuthorizationResponseUriAsync(IAuthenticator authenticator, - string authorizationUri, - CancellationToken cancellationToken = default) - { - if (authorizationCompletedTaskSource != null) - { - authorizationCompletedTaskSource.TrySetException(new AuthenticationException(Translator.Exception_AuthenticationCanceled)); - authorizationCompletedTaskSource = null; - } - - authorizationCompletedTaskSource = new TaskCompletionSource(); - - bool isLaunched = await Launcher.LaunchUriAsync(new Uri(authorizationUri)).AsTask(cancellationToken); - - if (!isLaunched) - throw new WinoServerException("Failed to launch Google Authentication dialog."); - - return await authorizationCompletedTaskSource.Task.WaitAsync(cancellationToken); - } - public void ContinueAuthorization(Uri authorizationResponseUri) { if (authorizationCompletedTaskSource != null) diff --git a/Wino.Core.UWP/Styles/DataTemplates.xaml b/Wino.Core.UWP/Styles/DataTemplates.xaml index 0191001c..1c5ca6fe 100644 --- a/Wino.Core.UWP/Styles/DataTemplates.xaml +++ b/Wino.Core.UWP/Styles/DataTemplates.xaml @@ -4,9 +4,9 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals" - xmlns:helpers="using:Wino.Helpers" xmlns:coreControls="using:Wino.Core.UWP.Controls" xmlns:domain="using:Wino.Core.Domain" + xmlns:helpers="using:Wino.Helpers" xmlns:local="using:Wino.Core.UWP.Styles" xmlns:menu="using:Wino.Core.MenuItems" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" diff --git a/Wino.Core/Authenticators/Base/BaseAuthenticator.cs b/Wino.Core/Authenticators/Base/BaseAuthenticator.cs deleted file mode 100644 index 9da7a83e..00000000 --- a/Wino.Core/Authenticators/Base/BaseAuthenticator.cs +++ /dev/null @@ -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); - } -} diff --git a/Wino.Core/Authenticators/Base/GmailAuthenticatorBase.cs b/Wino.Core/Authenticators/Base/GmailAuthenticatorBase.cs deleted file mode 100644 index c6e79d71..00000000 --- a/Wino.Core/Authenticators/Base/GmailAuthenticatorBase.cs +++ /dev/null @@ -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 GenerateTokenAsync(MailAccount account, bool saveToken); - - public abstract Task GetTokenAsync(MailAccount account); - } -} diff --git a/Wino.Core/Authenticators/Base/OutlookAuthenticatorBase.cs b/Wino.Core/Authenticators/Base/OutlookAuthenticatorBase.cs deleted file mode 100644 index b38982f3..00000000 --- a/Wino.Core/Authenticators/Base/OutlookAuthenticatorBase.cs +++ /dev/null @@ -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 GenerateTokenAsync(MailAccount account, bool saveToken); - - public abstract Task GetTokenAsync(MailAccount account); - } -} diff --git a/Wino.Core/Authenticators/Calendar/OutlookAuthenticator.cs b/Wino.Core/Authenticators/Calendar/OutlookAuthenticator.cs deleted file mode 100644 index 3d3f2658..00000000 --- a/Wino.Core/Authenticators/Calendar/OutlookAuthenticator.cs +++ /dev/null @@ -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 GenerateTokenAsync(MailAccount account, bool saveToken) - { - throw new NotImplementedException(); - } - - public override Task GetTokenAsync(MailAccount account) - { - throw new NotImplementedException(); - } - } -} diff --git a/Wino.Core/Authenticators/Mail/GmailAuthenticator.cs b/Wino.Core/Authenticators/Mail/GmailAuthenticator.cs deleted file mode 100644 index 127d2910..00000000 --- a/Wino.Core/Authenticators/Mail/GmailAuthenticator.cs +++ /dev/null @@ -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; - } - - /// - /// Performs tokenization code exchange and retrieves the actual Access - Refresh tokens from Google - /// after redirect uri returns from browser. - /// - /// Tokenization request. - /// In case of network or parsing related error. - private async Task 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()); - - var accessToken = parsed["access_token"].GetValue(); - var refreshToken = parsed["refresh_token"].GetValue(); - var expiresIn = parsed["expires_in"].GetValue(); - - 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()); - - var username = parsedUserInfo["emailAddress"].GetValue(); - - return new TokenInformation() - { - Id = Guid.NewGuid(), - Address = username, - AccessToken = accessToken, - RefreshToken = refreshToken, - ExpiresAt = expirationDate - }; - } - - public async override Task 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 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; - } - - /// - /// Internally exchanges refresh token with a new access token and returns new TokenInformation. - /// - /// Token to be used in refreshing. - /// New TokenInformationBase that has new tokens and expiration date without a username. This token is not saved to database after returned. - private async Task 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()); - - var accessToken = parsed["access_token"].GetValue(); - - 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(); - } - - var expiresIn = parsed["expires_in"].GetValue(); - var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn); - - return new TokenInformationBase() - { - AccessToken = accessToken, - ExpiresAt = expirationDate, - RefreshToken = activeRefreshToken - }; - } - } -} diff --git a/Wino.Core/Authenticators/Mail/Office365Authenticator.cs b/Wino.Core/Authenticators/Mail/Office365Authenticator.cs deleted file mode 100644 index 35bf51ed..00000000 --- a/Wino.Core/Authenticators/Mail/Office365Authenticator.cs +++ /dev/null @@ -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; - } -} diff --git a/Wino.Core/CoreContainerSetup.cs b/Wino.Core/CoreContainerSetup.cs index 4c6d7fe2..de6da3cf 100644 --- a/Wino.Core/CoreContainerSetup.cs +++ b/Wino.Core/CoreContainerSetup.cs @@ -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(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/Wino.Core/Extensions/TokenizationExtensions.cs b/Wino.Core/Extensions/TokenizationExtensions.cs deleted file mode 100644 index 46442d94..00000000 --- a/Wino.Core/Extensions/TokenizationExtensions.cs +++ /dev/null @@ -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; - } - } -} diff --git a/Wino.Core/Http/GmailClientMessageHandler.cs b/Wino.Core/Http/GmailClientMessageHandler.cs index f591c4b3..238c2f23 100644 --- a/Wino.Core/Http/GmailClientMessageHandler.cs +++ b/Wino.Core/Http/GmailClientMessageHandler.cs @@ -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> TokenRetrieveDelegate { get; } + private readonly IGmailAuthenticator _gmailAuthenticator; + private readonly MailAccount _mailAccount; - public GmailClientMessageHandler(Func> tokenRetrieveDelegate) : base(new HttpClientHandler()) + public GmailClientMessageHandler(IGmailAuthenticator gmailAuthenticator, MailAccount mailAccount) : base(new HttpClientHandler()) { - TokenRetrieveDelegate = tokenRetrieveDelegate; + _gmailAuthenticator = gmailAuthenticator; + _mailAccount = mailAccount; } protected override async Task 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); diff --git a/Wino.Core/Http/MicrosoftTokenProvider.cs b/Wino.Core/Http/MicrosoftTokenProvider.cs index ec373e1e..88df01b0 100644 --- a/Wino.Core/Http/MicrosoftTokenProvider.cs +++ b/Wino.Core/Http/MicrosoftTokenProvider.cs @@ -22,12 +22,12 @@ namespace Wino.Core.Http public AllowedHostsValidator AllowedHostsValidator { get; } public async Task GetAuthorizationTokenAsync(Uri uri, - Dictionary additionalAuthenticationContext = null, - CancellationToken cancellationToken = default) + Dictionary 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; } } } diff --git a/Wino.Core/Services/AccountService.cs b/Wino.Core/Services/AccountService.cs index 630dd3bf..aa07b63f 100644 --- a/Wino.Core/Services/AccountService.cs +++ b/Wino.Core/Services/AccountService.cs @@ -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().Where(a => a.AccountId == account.Id).DeleteAsync(); await Connection.Table().DeleteAsync(a => a.MailAccountId == account.Id); await Connection.Table().DeleteAsync(a => a.MailAccountId == account.Id); await Connection.Table().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 UpdateSynchronizationIdentifierAsync(Guid accountId, string newIdentifier) diff --git a/Wino.Core/Services/AuthenticationProvider.cs b/Wino.Core/Services/AuthenticationProvider.cs index 57b1a64b..42a7d8b8 100644 --- a/Wino.Core/Services/AuthenticationProvider.cs +++ b/Wino.Core/Services/AuthenticationProvider.cs @@ -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), }; } diff --git a/Wino.Core/Services/DatabaseService.cs b/Wino.Core/Services/DatabaseService.cs index 294736c6..4e368aea 100644 --- a/Wino.Core/Services/DatabaseService.cs +++ b/Wino.Core/Services/DatabaseService.cs @@ -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((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), diff --git a/Wino.Core/Services/TokenService.cs b/Wino.Core/Services/TokenService.cs deleted file mode 100644 index a425e8e0..00000000 --- a/Wino.Core/Services/TokenService.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Threading.Tasks; -using Wino.Core.Domain.Entities.Shared; - -namespace Wino.Core.Services -{ - public interface ITokenService - { - Task GetTokenInformationAsync(Guid accountId); - Task SaveTokenInformationAsync(Guid accountId, TokenInformation tokenInformation); - } - - public class TokenService : BaseDatabaseService, ITokenService - { - public TokenService(IDatabaseService databaseService) : base(databaseService) { } - - public Task GetTokenInformationAsync(Guid accountId) - => Connection.Table().FirstOrDefaultAsync(a => a.AccountId == accountId); - - public async Task SaveTokenInformationAsync(Guid accountId, TokenInformation tokenInformation) - { - // Delete all tokens for this account. - await Connection.Table().DeleteAsync(a => a.AccountId == accountId); - - // Save new token info to the account. - tokenInformation.AccountId = accountId; - - await Connection.InsertOrReplaceAsync(tokenInformation); - } - } -} diff --git a/Wino.Core/Synchronizers/BaseMailSynchronizer.cs b/Wino.Core/Synchronizers/BaseMailSynchronizer.cs index 1843a035..0f876dfc 100644 --- a/Wino.Core/Synchronizers/BaseMailSynchronizer.cs +++ b/Wino.Core/Synchronizers/BaseMailSynchronizer.cs @@ -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; diff --git a/Wino.Core/Synchronizers/Mail/GmailSynchronizer.cs b/Wino.Core/Synchronizers/Mail/GmailSynchronizer.cs index 74afc361..15a8966f 100644 --- a/Wino.Core/Synchronizers/Mail/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/Mail/GmailSynchronizer.cs @@ -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(); 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() diff --git a/Wino.Core/Synchronizers/Mail/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/Mail/OutlookSynchronizer.cs index 58efc6f3..3cc8e583 100644 --- a/Wino.Core/Synchronizers/Mail/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/Mail/OutlookSynchronizer.cs @@ -533,20 +533,20 @@ namespace Wino.Core.Synchronizers.Mail /// /// Get the user's display name. /// - /// Display name of the user. - private async Task GetSenderNameAsync() + /// Display name and address of the user. + private async Task> GetDisplayNameAndAddressAsync() { var userInfo = await _graphClient.Me.GetAsync(); - return userInfo.DisplayName; + return new Tuple(userInfo.DisplayName, userInfo.Mail); } public override async Task 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); } /// diff --git a/Wino.Core/Wino.Core.csproj b/Wino.Core/Wino.Core.csproj index 20fad4b2..19b1a7ae 100644 --- a/Wino.Core/Wino.Core.csproj +++ b/Wino.Core/Wino.Core.csproj @@ -43,6 +43,7 @@ + diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs index 86b296d9..8cab69a8 100644 --- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs @@ -14,6 +14,7 @@ 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.Navigation; using Wino.Core.Domain.Models.Synchronization; using Wino.Core.ViewModels; @@ -107,7 +108,7 @@ namespace Wino.Mail.ViewModels creationDialog.ShowDialog(accountCreationCancellationTokenSource); creationDialog.State = AccountCreationDialogState.SigningIn; - TokenInformation tokenInformation = null; + string tokenInformation = string.Empty; // Custom server implementation requires more async waiting. if (creationDialog is ICustomServerAccountCreationDialog customServerDialog) @@ -129,24 +130,26 @@ namespace Wino.Mail.ViewModels } else { - // For OAuth authentications, we just generate token and assign it to the MailAccount. + // OAuth authentication is handled here. + // Server authenticates, returns the token info here. var tokenInformationResponse = await WinoServerConnectionManager - .GetResponseAsync(new AuthorizationRequested(accountCreationDialogResult.ProviderType, + .GetResponseAsync(new AuthorizationRequested(accountCreationDialogResult.ProviderType, createdAccount, createdAccount.ProviderType == MailProviderType.Gmail), accountCreationCancellationTokenSource.Token); if (creationDialog.State == AccountCreationDialogState.Canceled) throw new AccountSetupCanceledException(); - tokenInformationResponse.ThrowIfFailed(); + createdAccount.Address = tokenInformationResponse.Data.AccountAddress; - tokenInformation = tokenInformationResponse.Data; - createdAccount.Address = tokenInformation.Address; - tokenInformation.AccountId = createdAccount.Id; + tokenInformationResponse.ThrowIfFailed(); } - await AccountService.CreateAccountAsync(createdAccount, tokenInformation, customServerInformation); + // Address is still doesn't have a value for API synchronizers. + // It'll be synchronized with profile information. + + await AccountService.CreateAccountAsync(createdAccount, customServerInformation); // Local account has been created. @@ -172,6 +175,11 @@ namespace Wino.Mail.ViewModels createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName; createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData; + if (!string.IsNullOrEmpty(profileSynchronizationResult.ProfileInformation.AccountAddress)) + { + createdAccount.Address = profileSynchronizationResult.ProfileInformation.AccountAddress; + } + await AccountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation); } diff --git a/Wino.Mail/App.xaml.cs b/Wino.Mail/App.xaml.cs index ec6c6997..c7e10e0e 100644 --- a/Wino.Mail/App.xaml.cs +++ b/Wino.Mail/App.xaml.cs @@ -96,7 +96,7 @@ namespace Wino services.AddSingleton(); services.AddTransient(); services.AddTransient(); - + services.AddSingleton(); } private void RegisterViewModels(IServiceCollection services) diff --git a/Wino.Mail/Package.appxmanifest b/Wino.Mail/Package.appxmanifest index c0457ee6..5407eb55 100644 --- a/Wino.Mail/Package.appxmanifest +++ b/Wino.Mail/Package.appxmanifest @@ -42,7 +42,7 @@ + EntryPoint="Wino.Mail.App"> "b19c2035-d740-49ff-b297-de6ec561b208"; + + public string[] OutlookScope => new string[] + { + "email", + "mail.readwrite", + "offline_access", + "mail.send", + "Mail.Send.Shared", + "Mail.ReadWrite.Shared", + "User.Read" + }; + + public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com"; + + public string[] GmailScope => new string[] + { + "https://mail.google.com/", + "https://www.googleapis.com/auth/userinfo.profile", + "https://www.googleapis.com/auth/gmail.labels" + }; + } +} diff --git a/Wino.Mail/Wino.Mail.csproj b/Wino.Mail/Wino.Mail.csproj index f854920a..1ba4ef5e 100644 --- a/Wino.Mail/Wino.Mail.csproj +++ b/Wino.Mail/Wino.Mail.csproj @@ -14,8 +14,8 @@ {68A432B8-C1B7-494C-8D6D-230788EA683E} AppContainerExe Properties - Wino - Wino + Wino.Mail + Wino.Mail en-US UAP 10.0.22621.0 @@ -259,6 +259,7 @@ + @@ -565,7 +566,6 @@ - @@ -623,9 +623,7 @@ Windows Desktop Extensions for the UWP - - - + 14.0 diff --git a/Wino.Server/App.xaml.cs b/Wino.Server/App.xaml.cs index 024dd156..5768fece 100644 --- a/Wino.Server/App.xaml.cs +++ b/Wino.Server/App.xaml.cs @@ -83,6 +83,19 @@ namespace Wino.Server services.AddSingleton(serverMessageHandlerFactory); + // Server type related services. + // TODO: Better abstraction. + + if (WinoServerType == WinoAppType.Mail) + { + services.AddSingleton(); + } + else + { + // TODO: Calendar config will be added here. + } + + return services.BuildServiceProvider(); } @@ -135,6 +148,8 @@ namespace Wino.Server { string processName = WinoServerType == WinoAppType.Mail ? "Wino.Mail" : "Wino.Calendar"; + var processs = Process.GetProcesses(); + var proc = Process.GetProcessesByName(processName).FirstOrDefault() ?? throw new Exception($"{processName} client is not running."); for (IntPtr appWindow = FindWindowEx(IntPtr.Zero, IntPtr.Zero, FRAME_WINDOW, null); appWindow != IntPtr.Zero; diff --git a/Wino.Server/MessageHandlers/AuthenticationHandler.cs b/Wino.Server/MessageHandlers/AuthenticationHandler.cs index 19afd5e0..fd9c5394 100644 --- a/Wino.Server/MessageHandlers/AuthenticationHandler.cs +++ b/Wino.Server/MessageHandlers/AuthenticationHandler.cs @@ -1,27 +1,27 @@ using System; using System.Threading; using System.Threading.Tasks; -using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Authentication; using Wino.Core.Domain.Models.Server; using Wino.Messaging.Server; using Wino.Server.Core; namespace Wino.Server.MessageHandlers { - public class AuthenticationHandler : ServerMessageHandler + public class AuthenticationHandler : ServerMessageHandler { private readonly IAuthenticationProvider _authenticationProvider; - public override WinoServerResponse FailureDefaultResponse(Exception ex) - => WinoServerResponse.CreateErrorResponse(ex.Message); + public override WinoServerResponse FailureDefaultResponse(Exception ex) + => WinoServerResponse.CreateErrorResponse(ex.Message); public AuthenticationHandler(IAuthenticationProvider authenticationProvider) { _authenticationProvider = authenticationProvider; } - protected override async Task> HandleAsync(AuthorizationRequested message, + protected override async Task> HandleAsync(AuthorizationRequested message, CancellationToken cancellationToken = default) { var authenticator = _authenticationProvider.GetAuthenticator(message.MailProviderType); @@ -36,10 +36,21 @@ namespace Wino.Server.MessageHandlers gmailAuthenticator.ProposeCopyAuthURL = true; } - // Do not save the token here. Call is coming from account creation and things are atomic there. - var generatedToken = await authenticator.GenerateTokenAsync(message.CreatedAccount, saveToken: false); + TokenInformationEx generatedToken = null; - return WinoServerResponse.CreateSuccessResponse(generatedToken); + if (message.CreatedAccount != null) + { + generatedToken = await authenticator.GetTokenInformationAsync(message.CreatedAccount); + } + else + { + // Initial authentication request. + // There is no account to get token for. + + generatedToken = await authenticator.GenerateTokenInformationAsync(message.CreatedAccount); + } + + return WinoServerResponse.CreateSuccessResponse(generatedToken); } } } diff --git a/Wino.Server/ServerViewModel.cs b/Wino.Server/ServerViewModel.cs index bb5899f2..2f7c7803 100644 --- a/Wino.Server/ServerViewModel.cs +++ b/Wino.Server/ServerViewModel.cs @@ -26,22 +26,7 @@ namespace Wino.Server [RelayCommand] public Task LaunchWinoAsync() { - //var opt = new SynchronizationOptions() - //{ - // Type = Wino.Core.Domain.Enums.SynchronizationType.Full, - // AccountId = Guid.Parse("b3620ce7-8a69-4d81-83d5-a94bbe177431") - //}; - - //var req = new NewSynchronizationRequested(opt, Wino.Core.Domain.Enums.SynchronizationSource.Server); - //WeakReferenceMessenger.Default.Send(req); - - // return Task.CompletedTask; - return Launcher.LaunchUriAsync(new Uri($"{App.WinoMailLaunchProtocol}:")).AsTask(); - //await _notificationBuilder.CreateNotificationsAsync(Guid.Empty, new List() - //{ - // new MailCopy(){ UniqueId = Guid.Parse("8f25d2a0-4448-4fee-96a9-c9b25a19e866")} - //}); } /// diff --git a/Wino.Server/Wino.Server.csproj b/Wino.Server/Wino.Server.csproj index 7f9edb8b..971e6885 100644 --- a/Wino.Server/Wino.Server.csproj +++ b/Wino.Server/Wino.Server.csproj @@ -18,6 +18,7 @@ + @@ -36,6 +37,7 @@ + diff --git a/Wino.sln b/Wino.sln index 61fc80b4..28c61a9b 100644 --- a/Wino.sln +++ b/Wino.sln @@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Core.ViewModels", "Win EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Calendar.ViewModels", "Wino.Calendar.ViewModels\Wino.Calendar.ViewModels.csproj", "{039AFFA8-C1CC-4E3B-8A31-6814D7557F74}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wino.Authentication", "Wino.Authentication\Wino.Authentication.csproj", "{A4DBA01A-F315-49E0-8428-BB99D32B20F9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -306,6 +308,26 @@ Global {039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x64.Build.0 = Release|Any CPU {039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x86.ActiveCfg = Release|Any CPU {039AFFA8-C1CC-4E3B-8A31-6814D7557F74}.Release|x86.Build.0 = Release|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|ARM.ActiveCfg = Debug|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|ARM.Build.0 = Debug|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|ARM64.Build.0 = Debug|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x64.Build.0 = Debug|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Debug|x86.Build.0 = Debug|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|Any CPU.Build.0 = Release|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|ARM.ActiveCfg = Release|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|ARM.Build.0 = Release|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|ARM64.ActiveCfg = Release|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|ARM64.Build.0 = Release|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x64.ActiveCfg = Release|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x64.Build.0 = Release|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x86.ActiveCfg = Release|Any CPU + {A4DBA01A-F315-49E0-8428-BB99D32B20F9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -320,6 +342,7 @@ Global {D4919A19-E70F-4916-83D2-5D5F87BEB949} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758} {53723AE8-7E7E-4D54-ADAB-0A6033255CC8} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758} {039AFFA8-C1CC-4E3B-8A31-6814D7557F74} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758} + {A4DBA01A-F315-49E0-8428-BB99D32B20F9} = {17FF5FAE-F1AC-4572-BAA3-8B86F01EA758} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {721F946E-F69F-4987-823A-D084B436FC1E}