- remove duplicate ITenantRepository/TenantRepository from Console - extend Platform ITenantRepository with GetByIdAsync, GetPagedAsync, CountAsync - update Console services to use Platform.Infrastructure.Repositories - fix nullable warnings (UserDto, OAuthClientService) - fix YarpGateway Directory.Build.props duplicate import - fix DynamicProxyConfigProvider CS8618 warning
312 lines
13 KiB
C#
312 lines
13 KiB
C#
using Fengling.Console.Models.Dtos;
|
|
using OpenIddict.Abstractions;
|
|
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
|
|
namespace Fengling.Console.Services;
|
|
|
|
public interface IOAuthClientService
|
|
{
|
|
Task<(IEnumerable<OAuthClientDto> Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null);
|
|
Task<OAuthClientDto?> GetClientAsync(string id);
|
|
Task<OAuthClientDto> CreateClientAsync(CreateClientDto dto);
|
|
Task<object> GenerateSecretAsync(string id);
|
|
Task<object> UpdateClientAsync(string id, UpdateClientDto dto);
|
|
Task DeleteClientAsync(string id);
|
|
object GetClientOptions();
|
|
}
|
|
|
|
public class OAuthClientService : IOAuthClientService
|
|
{
|
|
private readonly IOpenIddictApplicationManager _applicationManager;
|
|
private readonly HttpClient _httpClient;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ILogger<OAuthClientService> _logger;
|
|
|
|
public OAuthClientService(
|
|
IOpenIddictApplicationManager applicationManager,
|
|
HttpClient httpClient,
|
|
IConfiguration configuration,
|
|
ILogger<OAuthClientService> logger)
|
|
{
|
|
_applicationManager = applicationManager;
|
|
_httpClient = httpClient;
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<(IEnumerable<OAuthClientDto> Items, int TotalCount)> GetClientsAsync(int page, int pageSize, string? displayName = null, string? clientId = null, string? status = null)
|
|
{
|
|
var applications = _applicationManager.ListAsync();
|
|
var clientList = new List<OAuthClientDto>();
|
|
|
|
await foreach (var application in applications)
|
|
{
|
|
var clientIdValue = await _applicationManager.GetClientIdAsync(application);
|
|
var displayNameValue = await _applicationManager.GetDisplayNameAsync(application);
|
|
|
|
if (!string.IsNullOrEmpty(displayName) && (string.IsNullOrEmpty(displayNameValue) || !displayNameValue.Contains(displayName, StringComparison.OrdinalIgnoreCase)))
|
|
continue;
|
|
|
|
if (!string.IsNullOrEmpty(clientId) && (string.IsNullOrEmpty(clientIdValue) || !clientIdValue.Contains(clientId, StringComparison.OrdinalIgnoreCase)))
|
|
continue;
|
|
|
|
var clientType = await _applicationManager.GetClientTypeAsync(application);
|
|
var consentType = await _applicationManager.GetConsentTypeAsync(application);
|
|
var permissions = await _applicationManager.GetPermissionsAsync(application);
|
|
var redirectUris = await _applicationManager.GetRedirectUrisAsync(application);
|
|
var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
|
|
var applicationId = await _applicationManager.GetIdAsync(application);
|
|
|
|
clientList.Add(new OAuthClientDto
|
|
{
|
|
Id = applicationId ?? clientIdValue ?? string.Empty,
|
|
ClientId = clientIdValue ?? string.Empty,
|
|
DisplayName = displayNameValue ?? string.Empty,
|
|
RedirectUris = redirectUris.ToArray(),
|
|
PostLogoutRedirectUris = postLogoutRedirectUris.ToArray(),
|
|
Scopes = permissions
|
|
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
|
|
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length))
|
|
.ToArray(),
|
|
GrantTypes = permissions
|
|
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
|
|
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length))
|
|
.ToArray(),
|
|
ClientType = clientType?.ToString(),
|
|
ConsentType = consentType?.ToString(),
|
|
Status = "active"
|
|
});
|
|
}
|
|
|
|
var sortedClients = clientList
|
|
.OrderByDescending(c => c.ClientId)
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.ToList();
|
|
|
|
return (sortedClients, clientList.Count);
|
|
}
|
|
|
|
public async Task<OAuthClientDto?> GetClientAsync(string id)
|
|
{
|
|
var application = await _applicationManager.FindByIdAsync(id);
|
|
if (application == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var clientIdValue = await _applicationManager.GetClientIdAsync(application);
|
|
var displayNameValue = await _applicationManager.GetDisplayNameAsync(application);
|
|
var clientType = await _applicationManager.GetClientTypeAsync(application);
|
|
var consentType = await _applicationManager.GetConsentTypeAsync(application);
|
|
var permissions = await _applicationManager.GetPermissionsAsync(application);
|
|
var redirectUris = await _applicationManager.GetRedirectUrisAsync(application);
|
|
var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
|
|
|
|
return new OAuthClientDto
|
|
{
|
|
Id = id,
|
|
ClientId = clientIdValue ?? string.Empty,
|
|
DisplayName = displayNameValue ?? string.Empty,
|
|
RedirectUris = redirectUris.ToArray(),
|
|
PostLogoutRedirectUris = postLogoutRedirectUris.ToArray(),
|
|
Scopes = permissions
|
|
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
|
|
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length))
|
|
.ToArray(),
|
|
GrantTypes = permissions
|
|
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
|
|
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length))
|
|
.ToArray(),
|
|
ClientType = clientType?.ToString(),
|
|
ConsentType = consentType?.ToString(),
|
|
Status = "active"
|
|
};
|
|
}
|
|
|
|
public async Task<OAuthClientDto> CreateClientAsync(CreateClientDto dto)
|
|
{
|
|
var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132";
|
|
var token = await GetAuthTokenAsync();
|
|
|
|
var clientSecret = string.IsNullOrEmpty(dto.ClientSecret) ? GenerateSecureSecret() : dto.ClientSecret;
|
|
|
|
var requestBody = new
|
|
{
|
|
dto.ClientId,
|
|
ClientSecret = clientSecret,
|
|
dto.DisplayName,
|
|
dto.RedirectUris,
|
|
dto.PostLogoutRedirectUris,
|
|
dto.Scopes,
|
|
dto.GrantTypes,
|
|
dto.ClientType,
|
|
dto.ConsentType,
|
|
dto.Status,
|
|
dto.Description
|
|
};
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Post, $"{authServiceUrl}/api/OAuthClients")
|
|
{
|
|
Content = new StringContent(
|
|
System.Text.Json.JsonSerializer.Serialize(requestBody),
|
|
System.Text.Encoding.UTF8,
|
|
"application/json")
|
|
};
|
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
|
|
|
var response = await _httpClient.SendAsync(request);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var result = System.Text.Json.JsonSerializer.Deserialize<OAuthClientDto>(content);
|
|
|
|
_logger.LogInformation("Created OAuth client {ClientId}", dto.ClientId);
|
|
|
|
return result!;
|
|
}
|
|
|
|
public async Task<object> GenerateSecretAsync(string id)
|
|
{
|
|
var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132";
|
|
var token = await GetAuthTokenAsync();
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Post, $"{authServiceUrl}/api/OAuthClients/{id}/generate-secret");
|
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
|
|
|
var response = await _httpClient.SendAsync(request);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var result = System.Text.Json.JsonSerializer.Deserialize<object>(content);
|
|
|
|
_logger.LogInformation("Generated new secret for OAuth client {Id}", id);
|
|
|
|
return result!;
|
|
}
|
|
|
|
public async Task<object> UpdateClientAsync(string id, UpdateClientDto dto)
|
|
{
|
|
var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132";
|
|
var token = await GetAuthTokenAsync();
|
|
|
|
var requestBody = new
|
|
{
|
|
dto.DisplayName,
|
|
dto.RedirectUris,
|
|
dto.PostLogoutRedirectUris,
|
|
dto.Scopes,
|
|
dto.GrantTypes,
|
|
dto.ClientType,
|
|
dto.ConsentType,
|
|
dto.Status,
|
|
dto.Description
|
|
};
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Put, $"{authServiceUrl}/api/OAuthClients/{id}")
|
|
{
|
|
Content = new StringContent(
|
|
System.Text.Json.JsonSerializer.Serialize(requestBody),
|
|
System.Text.Encoding.UTF8,
|
|
"application/json")
|
|
};
|
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
|
|
|
var response = await _httpClient.SendAsync(request);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
_logger.LogInformation("Updated OAuth client {Id}", id);
|
|
|
|
return new { message = "Client updated successfully" };
|
|
}
|
|
|
|
public async Task DeleteClientAsync(string id)
|
|
{
|
|
var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132";
|
|
var token = await GetAuthTokenAsync();
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Delete, $"{authServiceUrl}/api/OAuthClients/{id}");
|
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
|
|
|
var response = await _httpClient.SendAsync(request);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
_logger.LogInformation("Deleted OAuth client {Id}", id);
|
|
}
|
|
|
|
public object GetClientOptions()
|
|
{
|
|
return new
|
|
{
|
|
clientTypes = new[]
|
|
{
|
|
new { value = "public", label = "Public (SPA, Mobile App)" },
|
|
new { value = "confidential", label = "Confidential (Server-side)" }
|
|
},
|
|
consentTypes = new[]
|
|
{
|
|
new { value = "implicit", label = "Implicit" },
|
|
new { value = "explicit", label = "Explicit" },
|
|
new { value = "system", label = "System (Pre-authorized)" }
|
|
},
|
|
grantTypes = new[]
|
|
{
|
|
new { value = "authorization_code", label = "Authorization Code" },
|
|
new { value = "client_credentials", label = "Client Credentials" },
|
|
new { value = "refresh_token", label = "Refresh Token" },
|
|
new { value = "password", label = "Resource Owner Password Credentials" }
|
|
},
|
|
scopes = new[]
|
|
{
|
|
new { value = "openid", label = "OpenID Connect" },
|
|
new { value = "profile", label = "Profile" },
|
|
new { value = "email", label = "Email" },
|
|
new { value = "api", label = "API Access" },
|
|
new { value = "offline_access", label = "Offline Access" }
|
|
},
|
|
statuses = new[]
|
|
{
|
|
new { value = "active", label = "Active" },
|
|
new { value = "inactive", label = "Inactive" },
|
|
new { value = "suspended", label = "Suspended" }
|
|
}
|
|
};
|
|
}
|
|
|
|
private async Task<string> GetAuthTokenAsync()
|
|
{
|
|
var authServiceUrl = _configuration["AuthService:Url"] ?? "http://localhost:5132";
|
|
var clientId = _configuration["AuthService:ClientId"] ?? "console-client";
|
|
var clientSecret = _configuration["AuthService:ClientSecret"] ?? "console-secret";
|
|
|
|
var requestBody = new Dictionary<string, string>
|
|
{
|
|
{ "grant_type", "client_credentials" },
|
|
{ "client_id", clientId },
|
|
{ "client_secret", clientSecret },
|
|
{ "scope", "api" }
|
|
};
|
|
|
|
var request = new HttpRequestMessage(HttpMethod.Post, $"{authServiceUrl}/connect/token")
|
|
{
|
|
Content = new FormUrlEncodedContent(requestBody)
|
|
};
|
|
|
|
var response = await _httpClient.SendAsync(request);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
var tokenResponse = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(content);
|
|
|
|
return tokenResponse!["access_token"].GetString() ?? string.Empty;
|
|
}
|
|
|
|
private static string GenerateSecureSecret(int length = 32)
|
|
{
|
|
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
var bytes = RandomNumberGenerator.GetBytes(length);
|
|
return new string(bytes.Select(b => chars[b % chars.Length]).ToArray());
|
|
}
|
|
}
|