Issue: Account selector dialog pops up endlessly for Outlook/Live accounts. (Stored account not being correctly identified) Fix: Ignore case differences, add null safety and remove whitespaces when retrieving stored accounts.
127 lines
4.8 KiB
C#
127 lines
4.8 KiB
C#
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.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;
|
|
|
|
namespace Wino.Authentication;
|
|
|
|
public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
|
|
{
|
|
private const string TokenCacheFileName = "OutlookCache.bin";
|
|
private bool isTokenCacheAttached = false;
|
|
|
|
// Outlook
|
|
private const string Authority = "https://login.microsoftonline.com/common";
|
|
|
|
public override MailProviderType ProviderType => MailProviderType.Outlook;
|
|
|
|
private readonly IPublicClientApplication _publicClientApplication;
|
|
private readonly IApplicationConfiguration _applicationConfiguration;
|
|
|
|
public OutlookAuthenticator(INativeAppService nativeAppService,
|
|
IApplicationConfiguration applicationConfiguration,
|
|
IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig)
|
|
{
|
|
_applicationConfiguration = applicationConfiguration;
|
|
|
|
var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri();
|
|
|
|
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
|
|
{
|
|
Title = "Wino Mail",
|
|
ListOperatingSystemAccounts = true,
|
|
};
|
|
|
|
var outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
|
|
.WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd)
|
|
.WithBroker(options)
|
|
.WithDefaultRedirectUri()
|
|
.WithAuthority(Authority);
|
|
|
|
_publicClientApplication = outlookAppBuilder.Build();
|
|
}
|
|
|
|
public string[] Scope => AuthenticatorConfig.OutlookScope;
|
|
|
|
private async Task EnsureTokenCacheAttachedAsync()
|
|
{
|
|
if (!isTokenCacheAttached)
|
|
{
|
|
var storageProperties = new StorageCreationPropertiesBuilder(TokenCacheFileName, _applicationConfiguration.PublisherSharedFolderPath).Build();
|
|
var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties);
|
|
msalcachehelper.RegisterCache(_publicClientApplication.UserTokenCache);
|
|
|
|
isTokenCacheAttached = true;
|
|
}
|
|
}
|
|
|
|
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
|
|
{
|
|
await EnsureTokenCacheAttachedAsync();
|
|
|
|
var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(
|
|
a => string.Equals(a.Username?.Trim(), account.Address?.Trim(), StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (storedAccount == null)
|
|
return await GenerateTokenInformationAsync(account);
|
|
|
|
try
|
|
{
|
|
var authResult = await _publicClientApplication.AcquireTokenSilent(Scope, storedAccount).ExecuteAsync();
|
|
|
|
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 GenerateTokenInformationAsync(account);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
|
|
{
|
|
try
|
|
{
|
|
await EnsureTokenCacheAttachedAsync();
|
|
|
|
var authResult = await _publicClientApplication
|
|
.AcquireTokenInteractive(Scope)
|
|
.ExecuteAsync();
|
|
|
|
// 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 (account?.Address != null && !account.Address.Equals(authResult.Account.Username, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new AuthenticationException("Authenticated address does not match with your account address. If you are signing with a Office365, it is not officially supported yet.");
|
|
}
|
|
|
|
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
|
|
}
|
|
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));
|
|
}
|
|
}
|