153 lines
6.1 KiB
C#
153 lines
6.1 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 INativeAppService _nativeAppService;
|
|
private readonly IApplicationConfiguration _applicationConfiguration;
|
|
|
|
public OutlookAuthenticator(INativeAppService nativeAppService,
|
|
IApplicationConfiguration applicationConfiguration,
|
|
IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig)
|
|
{
|
|
_nativeAppService = nativeAppService;
|
|
_applicationConfiguration = applicationConfiguration;
|
|
|
|
var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri();
|
|
|
|
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
|
|
{
|
|
Title = "Wino Mail",
|
|
ListOperatingSystemAccounts = true,
|
|
};
|
|
|
|
PublicClientApplicationBuilder outlookAppBuilder = null;
|
|
|
|
// Being created from an app notification.
|
|
// This is where we avoid all interactive shit for authentication.
|
|
if (nativeAppService.GetCoreWindowHwnd == null)
|
|
{
|
|
outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
|
|
.WithDefaultRedirectUri()
|
|
.WithBroker(options)
|
|
.WithAuthority(Authority);
|
|
}
|
|
else
|
|
{
|
|
outlookAppBuilder = PublicClientApplicationBuilder.Create(AuthenticatorConfig.OutlookAuthenticatorClientId)
|
|
.WithBroker(options)
|
|
.WithParentActivityOrWindow(_nativeAppService.GetCoreWindowHwnd)
|
|
.WithDefaultRedirectUri()
|
|
.WithAuthority(Authority);
|
|
}
|
|
|
|
_publicClientApplication = outlookAppBuilder.Build();
|
|
}
|
|
|
|
private string[] GetScope(MailAccount account)
|
|
=> AuthenticatorConfig.GetOutlookScope(
|
|
account?.IsMailAccessGranted != false,
|
|
account?.IsCalendarAccessGranted == true);
|
|
|
|
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(GetScope(account), 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 which will include calendar scopes.
|
|
// The calling code should update account.IsCalendarAccessGranted = true after successful authentication.
|
|
|
|
return await GenerateTokenInformationAsync(account);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
|
|
{
|
|
try
|
|
{
|
|
await EnsureTokenCacheAttachedAsync();
|
|
|
|
// Interactive authentication required but window doesn't exist.
|
|
// This can happen when being called from a notification background task and the token is expired.
|
|
// Force account attention;
|
|
|
|
if (_nativeAppService.GetCoreWindowHwnd == null) throw new AuthenticationAttentionException(account);
|
|
|
|
AuthenticationResult authResult = await _publicClientApplication
|
|
.AcquireTokenInteractive(GetScope(account))
|
|
.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));
|
|
}
|
|
}
|