Office365 AuthenticationRecord persistieren, Graph-Scope Fix, SIP Expiry 3600s
This commit is contained in:
parent
3b2f149d8f
commit
ed18ffdd1d
3 changed files with 68 additions and 16 deletions
|
|
@ -6,7 +6,7 @@
|
|||
"Username": "",
|
||||
"Password": "",
|
||||
"DisplayName": "",
|
||||
"RegisterExpirySeconds": 300,
|
||||
"RegisterExpirySeconds": 3600,
|
||||
"SendRingingProgress": true
|
||||
},
|
||||
"Contacts": {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.IO;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
|
@ -8,7 +9,10 @@ namespace Anrufmonitor.Contacts;
|
|||
|
||||
/// <summary>
|
||||
/// Liest die Outlook/Office365-Kontakte des angemeldeten Benutzers ueber Microsoft Graph.
|
||||
/// Anmeldung per Device-Code-Flow (interaktiv, einmalig, Token wird vom MSAL-Cache verwaltet).
|
||||
/// Anmeldung per Device-Code-Flow. Nur beim allerersten Mal interaktiv: der MSAL-Token-Cache
|
||||
/// UND der <see cref="AuthenticationRecord"/> werden lokal persistiert (letzterer sagt MSAL,
|
||||
/// welcher Account beim naechsten Start automatisch/silent verwendet werden soll - ohne ihn
|
||||
/// wuerde trotz persistiertem Cache jedes Mal wieder interaktiv nachgefragt).
|
||||
/// Benoetigte App-Registration (Azure/Entra Portal): Public client, Delegated Permission "Contacts.Read".
|
||||
/// </summary>
|
||||
public sealed class Office365ContactProvider : IContactProvider
|
||||
|
|
@ -29,6 +33,12 @@ public sealed class Office365ContactProvider : IContactProvider
|
|||
/// <summary>Feuert mit der fuer den Benutzer bestimmten Anmelde-Nachricht (Code + URL) des Device-Code-Flows.</summary>
|
||||
public event Action<string>? DeviceCodeRequired;
|
||||
|
||||
private static string AuthRecordPath =>
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Anrufmonitor",
|
||||
"office365-auth-record.json");
|
||||
|
||||
public async Task<IReadOnlyList<ContactInfo>> GetAllContactsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
|
|
@ -36,19 +46,7 @@ public sealed class Office365ContactProvider : IContactProvider
|
|||
return [];
|
||||
}
|
||||
|
||||
var credential = new DeviceCodeCredential(new DeviceCodeCredentialOptions
|
||||
{
|
||||
ClientId = _options.Office365.ClientId,
|
||||
TenantId = _options.Office365.TenantId,
|
||||
DeviceCodeCallback = (info, ct) =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Office365-Anmeldung erforderlich: {Message}",
|
||||
info.Message);
|
||||
DeviceCodeRequired?.Invoke(info.Message);
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
});
|
||||
var credential = await CreateCredentialAsync(cancellationToken);
|
||||
|
||||
var graphClient = new GraphServiceClient(credential, ["Contacts.Read"]);
|
||||
|
||||
|
|
@ -78,6 +76,60 @@ public sealed class Office365ContactProvider : IContactProvider
|
|||
return result;
|
||||
}
|
||||
|
||||
private async Task<DeviceCodeCredential> CreateCredentialAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
AuthenticationRecord? authRecord = null;
|
||||
|
||||
if (File.Exists(AuthRecordPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var readStream = File.OpenRead(AuthRecordPath);
|
||||
authRecord = await AuthenticationRecord.DeserializeAsync(readStream, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Gespeicherter Office365-AuthenticationRecord konnte nicht gelesen werden, melde erneut interaktiv an");
|
||||
}
|
||||
}
|
||||
|
||||
var credential = new DeviceCodeCredential(new DeviceCodeCredentialOptions
|
||||
{
|
||||
ClientId = _options.Office365.ClientId,
|
||||
TenantId = _options.Office365.TenantId,
|
||||
TokenCachePersistenceOptions = new TokenCachePersistenceOptions
|
||||
{
|
||||
Name = "Anrufmonitor.Office365",
|
||||
},
|
||||
AuthenticationRecord = authRecord,
|
||||
DeviceCodeCallback = (info, ct) =>
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Office365-Anmeldung erforderlich: {Message}",
|
||||
info.Message);
|
||||
DeviceCodeRequired?.Invoke(info.Message);
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
});
|
||||
|
||||
if (authRecord is null)
|
||||
{
|
||||
// Erste Anmeldung ueberhaupt (oder Record fehlt/ist kaputt): einmal interaktiv
|
||||
// authentifizieren und den Record fuer alle folgenden Starts wegschreiben.
|
||||
var newRecord = await credential.AuthenticateAsync(
|
||||
new global::Azure.Core.TokenRequestContext(["https://graph.microsoft.com/Contacts.Read"]),
|
||||
cancellationToken);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(AuthRecordPath)!);
|
||||
await using var writeStream = File.Create(AuthRecordPath);
|
||||
await newRecord.SerializeAsync(writeStream, cancellationToken);
|
||||
|
||||
_logger.LogInformation("Office365-Anmeldung gespeichert fuer {Account}", newRecord.Username);
|
||||
}
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
private static ContactInfo MapContact(Contact contact)
|
||||
{
|
||||
var numbers = new List<string>();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public sealed class SipOptions
|
|||
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
public int RegisterExpirySeconds { get; set; } = 300;
|
||||
public int RegisterExpirySeconds { get; set; } = 3600;
|
||||
|
||||
/// <summary>
|
||||
/// Passiv = nur mitlesen. Es wird nie geantwortet (200 OK), der Anruf wird also nie
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue