forked from gongxuegit/tiku-backend.net
feat: modularize external providers
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
@@ -12,12 +13,13 @@ public sealed class AuthService(
|
||||
IPasswordHasher passwordHasher,
|
||||
ISmsVerificationService smsVerificationService,
|
||||
ISessionService sessionService,
|
||||
IWechatOAuthClient wechatOAuthClient) : IAuthService
|
||||
IWechatOAuthClient wechatOAuthClient,
|
||||
ITenantExternalProviderConfigService providerConfigService) : IAuthService
|
||||
{
|
||||
private const string PasswordProvider = "password";
|
||||
private const string SmsProvider = "sms";
|
||||
private const string WechatWebProvider = "wechat_web";
|
||||
private const string WechatMiniAppProvider = "wechat-miniapp";
|
||||
private const string WechatMiniAppProvider = "wechat_miniapp";
|
||||
private static readonly string[] WechatWebProviderAliases = ["wechat_web", "wechat-web", "wechat"];
|
||||
private static readonly string[] WechatMiniAppProviderAliases = ["wechat-miniapp", "wechat_miniapp", "wechat-mini", "wechatMiniapp"];
|
||||
private static readonly string[] WechatIdentityProviders = ["wechat_web", "wechat-web", "wechat", "wechat-miniapp", "wechat_miniapp", "wechat-mini", "wechatMiniapp"];
|
||||
@@ -356,24 +358,30 @@ public sealed class AuthService(
|
||||
IReadOnlyList<string> aliases,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var rows = await dbContext.TenantAuthProviders
|
||||
.Where(entity =>
|
||||
entity.TenantId == tenantId &&
|
||||
aliases.Contains(entity.Provider) &&
|
||||
(entity.Status == TenantAuthProviderStatus.Active ||
|
||||
entity.Status == TenantAuthProviderStatus.Testing))
|
||||
.ToListAsync(cancellationToken);
|
||||
var row = aliases
|
||||
.Select(alias => rows.FirstOrDefault(entity => entity.Provider == alias))
|
||||
.FirstOrDefault(entity => entity is not null);
|
||||
TenantExternalProviderAccount? account = null;
|
||||
foreach (var alias in aliases)
|
||||
{
|
||||
try
|
||||
{
|
||||
account = await providerConfigService.GetActiveProviderAsync(
|
||||
tenantId,
|
||||
TenantExternalProviderCapability.Identity,
|
||||
alias,
|
||||
cancellationToken);
|
||||
break;
|
||||
}
|
||||
catch (TenantExternalProviderException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (row is null)
|
||||
if (account is null)
|
||||
{
|
||||
throw new AuthProviderNotConfiguredException(provider);
|
||||
}
|
||||
|
||||
var appId = GetJsonString(row.ConfigPublic, "appId", "clientId");
|
||||
var appSecret = GetJsonString(row.ConfigPublic, "appSecret", "clientSecret", "secret");
|
||||
var appId = GetJsonString(account.ConfigPublic, "appId", "clientId");
|
||||
var appSecret = GetJsonString(account.SecretPayload, "appSecret", "clientSecret", "secret");
|
||||
if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(appSecret))
|
||||
{
|
||||
throw new AuthProviderNotConfiguredException(provider);
|
||||
|
||||
14
Tiku.Infrastructure/Auth/NoopSmsProvider.cs
Normal file
14
Tiku.Infrastructure/Auth/NoopSmsProvider.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Tiku.Application.Auth;
|
||||
|
||||
namespace Tiku.Infrastructure.Auth;
|
||||
|
||||
internal sealed class NoopSmsProvider : ISmsProvider
|
||||
{
|
||||
public Task<SmsProviderSendResult> SendAsync(
|
||||
SmsProviderSendRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(new SmsProviderSendResult("noop", "accepted"));
|
||||
}
|
||||
}
|
||||
54
Tiku.Infrastructure/Auth/SelfHostedIdentityProvider.cs
Normal file
54
Tiku.Infrastructure/Auth/SelfHostedIdentityProvider.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Tiku.Application.Auth;
|
||||
|
||||
namespace Tiku.Infrastructure.Auth;
|
||||
|
||||
internal sealed class SelfHostedIdentityProvider(IAuthService authService) : IIdentityProvider
|
||||
{
|
||||
public async Task<IdentityProviderResult> AuthenticateAsync(
|
||||
IdentityProviderRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var provider = request.Provider.Trim().ToLowerInvariant().Replace("-", "_", StringComparison.Ordinal);
|
||||
var authenticated = provider switch
|
||||
{
|
||||
"password" => await authService.LoginWithPasswordAsync(
|
||||
new PasswordLoginRequest(
|
||||
request.TenantId,
|
||||
request.Identifier,
|
||||
request.Secret,
|
||||
request.IpAddress,
|
||||
request.UserAgent),
|
||||
cancellationToken),
|
||||
"sms" => await authService.LoginWithSmsAsync(
|
||||
new SmsLoginRequest(
|
||||
request.TenantId,
|
||||
request.Identifier,
|
||||
request.Secret,
|
||||
request.IpAddress,
|
||||
request.UserAgent),
|
||||
cancellationToken),
|
||||
"wechat_web" => await authService.LoginWithWechatWebAsync(
|
||||
new WechatLoginRequest(
|
||||
request.TenantId,
|
||||
request.Secret,
|
||||
request.IpAddress,
|
||||
request.UserAgent),
|
||||
cancellationToken),
|
||||
"wechat_miniapp" => await authService.LoginWithWechatMiniAppAsync(
|
||||
new WechatLoginRequest(
|
||||
request.TenantId,
|
||||
request.Secret,
|
||||
request.IpAddress,
|
||||
request.UserAgent),
|
||||
cancellationToken),
|
||||
_ => throw new AuthProviderNotConfiguredException(provider)
|
||||
};
|
||||
|
||||
return new IdentityProviderResult(
|
||||
provider,
|
||||
authenticated.UserId.ToString("N"),
|
||||
authenticated.Phone,
|
||||
authenticated.Email,
|
||||
authenticated.Name);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Auth;
|
||||
|
||||
public sealed class SmsVerificationService(TikuDbContext dbContext) : ISmsVerificationService
|
||||
public sealed class SmsVerificationService(
|
||||
TikuDbContext dbContext,
|
||||
ISmsProvider smsProvider) : ISmsVerificationService
|
||||
{
|
||||
private const int MaxPhoneRequestsPerHour = 5;
|
||||
private static readonly TimeSpan CodeLifetime = TimeSpan.FromMinutes(10);
|
||||
@@ -42,13 +44,22 @@ public sealed class SmsVerificationService(TikuDbContext dbContext) : ISmsVerifi
|
||||
rateLimit.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
var code = Random.Shared.Next(100000, 999999).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
var sendResult = await smsProvider.SendAsync(
|
||||
new SmsProviderSendRequest(
|
||||
request.TenantId,
|
||||
phone,
|
||||
request.Purpose,
|
||||
code,
|
||||
request.IpAddress,
|
||||
request.UserAgent),
|
||||
cancellationToken);
|
||||
var verification = new SmsVerificationCode
|
||||
{
|
||||
TenantId = request.TenantId,
|
||||
Phone = phone,
|
||||
Purpose = request.Purpose,
|
||||
CodeHash = SmsCodeHashing.Hash(request.TenantId, phone, request.Purpose, code),
|
||||
Provider = "mock",
|
||||
Provider = sendResult.Provider,
|
||||
Status = SmsVerificationStatus.Sent,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.Add(CodeLifetime),
|
||||
IpAddress = request.IpAddress,
|
||||
|
||||
Reference in New Issue
Block a user