2024-04-18 01:44:37 +02:00
|
|
|
using System;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using Microsoft.Identity.Client;
|
2024-08-05 00:36:26 +02:00
|
|
|
using Microsoft.Identity.Client.Broker;
|
|
|
|
|
using Microsoft.Identity.Client.Extensions.Msal;
|
2024-04-18 01:44:37 +02:00
|
|
|
using Wino.Core.Domain;
|
2024-11-10 23:28:25 +01:00
|
|
|
using Wino.Core.Domain.Entities.Shared;
|
2024-04-18 01:44:37 +02:00
|
|
|
using Wino.Core.Domain.Enums;
|
|
|
|
|
using Wino.Core.Domain.Exceptions;
|
|
|
|
|
using Wino.Core.Domain.Interfaces;
|
2024-11-20 01:45:48 +01:00
|
|
|
using Wino.Core.Domain.Models.Authentication;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
namespace Wino.Authentication;
|
|
|
|
|
|
|
|
|
|
public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
private const string TokenCacheFileName = "OutlookCache.bin";
|
|
|
|
|
private bool isTokenCacheAttached = false;
|
2024-08-05 00:36:26 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
// Outlook
|
|
|
|
|
private const string Authority = "https://login.microsoftonline.com/common";
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
public override MailProviderType ProviderType => MailProviderType.Outlook;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
private readonly IPublicClientApplication _publicClientApplication;
|
2025-11-14 11:37:26 +01:00
|
|
|
private readonly INativeAppService _nativeAppService;
|
2025-02-16 11:54:23 +01:00
|
|
|
private readonly IApplicationConfiguration _applicationConfiguration;
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
public OutlookAuthenticator(INativeAppService nativeAppService,
|
|
|
|
|
IApplicationConfiguration applicationConfiguration,
|
|
|
|
|
IAuthenticatorConfig authenticatorConfig) : base(authenticatorConfig)
|
|
|
|
|
{
|
2025-11-14 11:37:26 +01:00
|
|
|
_nativeAppService = nativeAppService;
|
2025-02-16 11:54:23 +01:00
|
|
|
_applicationConfiguration = applicationConfiguration;
|
2024-08-05 00:36:26 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
|
|
|
|
|
{
|
|
|
|
|
Title = "Wino Mail",
|
|
|
|
|
ListOperatingSystemAccounts = true,
|
|
|
|
|
};
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
PublicClientApplicationBuilder outlookAppBuilder = null;
|
2025-10-06 17:46:00 +02:00
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
// 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)
|
2025-11-14 13:29:49 +01:00
|
|
|
.WithParentActivityOrWindow(_nativeAppService.GetCoreWindowHwnd)
|
2025-11-14 11:37:26 +01:00
|
|
|
.WithDefaultRedirectUri()
|
|
|
|
|
.WithAuthority(Authority);
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
_publicClientApplication = outlookAppBuilder.Build();
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-20 19:38:30 +02:00
|
|
|
private string[] GetScope(MailAccount account)
|
|
|
|
|
=> AuthenticatorConfig.GetOutlookScope(
|
|
|
|
|
account?.IsMailAccessGranted != false,
|
|
|
|
|
account?.IsCalendarAccessGranted == true);
|
2024-11-20 01:45:48 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
private async Task EnsureTokenCacheAttachedAsync()
|
|
|
|
|
{
|
|
|
|
|
if (!isTokenCacheAttached)
|
2024-04-18 01:44:37 +02:00
|
|
|
{
|
2025-02-16 11:54:23 +01:00
|
|
|
var storageProperties = new StorageCreationPropertiesBuilder(TokenCacheFileName, _applicationConfiguration.PublisherSharedFolderPath).Build();
|
|
|
|
|
var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties);
|
|
|
|
|
msalcachehelper.RegisterCache(_publicClientApplication.UserTokenCache);
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
isTokenCacheAttached = true;
|
2024-11-20 01:45:48 +01:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
}
|
2024-11-20 01:45:48 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
public async Task<TokenInformationEx> GetTokenInformationAsync(MailAccount account)
|
|
|
|
|
{
|
|
|
|
|
await EnsureTokenCacheAttachedAsync();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-05-02 20:12:45 +10:00
|
|
|
var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(
|
|
|
|
|
a => string.Equals(a.Username?.Trim(), account.Address?.Trim(), StringComparison.OrdinalIgnoreCase));
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
if (storedAccount == null)
|
|
|
|
|
return await GenerateTokenInformationAsync(account);
|
2024-11-20 01:45:48 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
try
|
|
|
|
|
{
|
2026-04-20 19:38:30 +02:00
|
|
|
var authResult = await _publicClientApplication.AcquireTokenSilent(GetScope(account), storedAccount).ExecuteAsync();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
|
2025-02-16 11:35:43 +01:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
catch (MsalUiRequiredException)
|
|
|
|
|
{
|
|
|
|
|
// Somehow MSAL is not able to refresh the token silently.
|
2026-01-05 00:21:07 +01:00
|
|
|
// Force interactive login which will include calendar scopes.
|
|
|
|
|
// The calling code should update account.IsCalendarAccessGranted = true after successful authentication.
|
2024-11-20 01:45:48 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
return await GenerateTokenInformationAsync(account);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
public async Task<TokenInformationEx> GenerateTokenInformationAsync(MailAccount account)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await EnsureTokenCacheAttachedAsync();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2025-11-14 12:12:13 +01:00
|
|
|
// 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);
|
|
|
|
|
|
2025-11-14 11:37:26 +01:00
|
|
|
AuthenticationResult authResult = await _publicClientApplication
|
2026-04-20 19:38:30 +02:00
|
|
|
.AcquireTokenInteractive(GetScope(account))
|
2025-02-16 11:54:23 +01:00
|
|
|
.ExecuteAsync();
|
2024-04-18 01:44:37 +02:00
|
|
|
|
2026-04-22 01:43:53 +02:00
|
|
|
// Microsoft 365 work/school tenants can use a sign-in UPN that differs from
|
|
|
|
|
// the mailbox primary SMTP address, so interactive reauth must not reject them.
|
2025-02-16 11:35:43 +01:00
|
|
|
|
2025-02-16 11:54:23 +01:00
|
|
|
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
|
2025-02-16 11:35:43 +01:00
|
|
|
}
|
2025-02-16 11:54:23 +01:00
|
|
|
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));
|
2024-04-18 01:44:37 +02:00
|
|
|
}
|
|
|
|
|
}
|