feat(notifications): configurable OAuth2 authority + scope per SMTP configuration (defaults preserve M365) — entity + EF + migration + token service
Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -38,11 +38,22 @@ public class OAuth2TokenService
|
||||
/// Credentials format: "tenantId:clientId:clientSecret"
|
||||
/// </summary>
|
||||
/// <param name="credentials">Colon-separated string in the form <c>tenantId:clientId:clientSecret</c>.</param>
|
||||
/// <param name="authority">
|
||||
/// Optional token-endpoint authority template (may contain a <c>{tenant}</c> placeholder);
|
||||
/// null preserves the Microsoft 365 default endpoint.
|
||||
/// </param>
|
||||
/// <param name="scope">Optional OAuth2 scope; null preserves the Microsoft 365 default scope.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>A valid OAuth2 access token string.</returns>
|
||||
public async Task<string> GetTokenAsync(string credentials, CancellationToken cancellationToken = default)
|
||||
public async Task<string> GetTokenAsync(
|
||||
string credentials,
|
||||
string? authority = null,
|
||||
string? scope = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var key = CredentialKey(credentials);
|
||||
// Cache key incorporates authority + scope so two configurations with the same
|
||||
// credentials but different endpoints/scopes never share a token.
|
||||
var key = CredentialKey($"{credentials}|{authority}|{scope}");
|
||||
var entry = _cache.GetOrAdd(key, _ => new CacheEntry());
|
||||
|
||||
if (entry.Token != null && DateTimeOffset.UtcNow < entry.Expiry)
|
||||
@@ -70,14 +81,22 @@ public class OAuth2TokenService
|
||||
var clientSecret = parts[2];
|
||||
|
||||
var client = _httpClientFactory.CreateClient("OAuth2");
|
||||
var tokenUrl = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token";
|
||||
|
||||
// Authority: null → today's hardcoded M365 endpoint; otherwise the stored
|
||||
// template with an optional {tenant} placeholder resolved from the credential.
|
||||
var tokenUrl = authority == null
|
||||
? $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token"
|
||||
: authority.Replace("{tenant}", tenantId);
|
||||
|
||||
// Scope: null → the M365 default.
|
||||
var tokenScope = scope ?? "https://outlook.office365.com/.default";
|
||||
|
||||
var form = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "client_credentials",
|
||||
["client_id"] = clientId,
|
||||
["client_secret"] = clientSecret,
|
||||
["scope"] = "https://outlook.office365.com/.default"
|
||||
["scope"] = tokenScope
|
||||
});
|
||||
|
||||
var response = await client.PostAsync(tokenUrl, form, cancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user