添加OAuth2认证相关配置文件和服务实现,包括环境变量配置、PKCE流程支持、token管理等功能。主要变更: - 新增OAuth2配置文件 - 实现OAuth2服务层 - 更新请求拦截器支持token自动刷新 - 修改认证API和store以支持OAuth2流程
431 lines
17 KiB
C#
431 lines
17 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using OpenIddict.Abstractions;
|
|
using System.Security.Cryptography;
|
|
|
|
namespace Fengling.AuthService.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
[Authorize]
|
|
public class OAuthClientsController : ControllerBase
|
|
{
|
|
private readonly IOpenIddictApplicationManager _applicationManager;
|
|
private readonly ILogger<OAuthClientsController> _logger;
|
|
|
|
public OAuthClientsController(
|
|
IOpenIddictApplicationManager applicationManager,
|
|
ILogger<OAuthClientsController> logger)
|
|
{
|
|
_applicationManager = applicationManager;
|
|
_logger = logger;
|
|
}
|
|
|
|
[HttpGet]
|
|
public async Task<ActionResult<object>> GetClients(
|
|
[FromQuery] int page = 1,
|
|
[FromQuery] int pageSize = 10,
|
|
[FromQuery] string? displayName = null,
|
|
[FromQuery] string? clientId = null)
|
|
{
|
|
var applications = _applicationManager.ListAsync();
|
|
var clientList = new List<object>();
|
|
|
|
await foreach (var application in applications)
|
|
{
|
|
var clientIdValue = await _applicationManager.GetClientIdAsync(application);
|
|
var displayNameValue = await _applicationManager.GetDisplayNameAsync(application);
|
|
|
|
if (!string.IsNullOrEmpty(displayName) && !displayNameValue.Contains(displayName, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
|
|
if (!string.IsNullOrEmpty(clientId) && !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 redirectUrisStrings = await _applicationManager.GetRedirectUrisAsync(application);
|
|
var postLogoutRedirectUrisStrings = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
|
|
var redirectUris = redirectUrisStrings;
|
|
var postLogoutRedirectUris = postLogoutRedirectUrisStrings;
|
|
|
|
clientList.Add(new
|
|
{
|
|
id = application,
|
|
clientId = clientIdValue,
|
|
displayName = displayNameValue,
|
|
redirectUris = redirectUris.Select(u => u.ToString()).ToArray(),
|
|
postLogoutRedirectUris = postLogoutRedirectUris.Select(u => u.ToString()).ToArray(),
|
|
scopes = permissions
|
|
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
|
|
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)),
|
|
grantTypes = permissions
|
|
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
|
|
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)),
|
|
clientType = clientType?.ToString(),
|
|
consentType = consentType?.ToString(),
|
|
status = "active",
|
|
permissions
|
|
});
|
|
}
|
|
|
|
var sortedClients = clientList
|
|
.OrderByDescending(c => (c as dynamic).clientId)
|
|
.Skip((page - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.ToList();
|
|
|
|
return Ok(new
|
|
{
|
|
items = sortedClients,
|
|
totalCount = clientList.Count,
|
|
page,
|
|
pageSize
|
|
});
|
|
}
|
|
|
|
[HttpGet("options")]
|
|
public ActionResult<object> GetClientOptions()
|
|
{
|
|
return Ok(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" }
|
|
}
|
|
});
|
|
}
|
|
|
|
[HttpGet("{id}")]
|
|
public async Task<ActionResult<object>> GetClient(string id)
|
|
{
|
|
var application = await _applicationManager.FindByIdAsync(id);
|
|
if (application == null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
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 Ok(new
|
|
{
|
|
id = id,
|
|
clientId = clientIdValue,
|
|
displayName = displayNameValue,
|
|
redirectUris = redirectUris.Select(u => u.ToString()).ToArray(),
|
|
postLogoutRedirectUris = postLogoutRedirectUris.Select(u => u.ToString()).ToArray(),
|
|
scopes = permissions
|
|
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
|
|
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length)),
|
|
grantTypes = permissions
|
|
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
|
|
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length)),
|
|
clientType = clientType?.ToString(),
|
|
consentType = consentType?.ToString(),
|
|
status = "active",
|
|
permissions
|
|
});
|
|
}
|
|
|
|
[HttpPost]
|
|
public async Task<ActionResult<object>> CreateClient([FromBody] CreateOAuthClientDto dto)
|
|
{
|
|
var existingClient = await _applicationManager.FindByClientIdAsync(dto.ClientId);
|
|
if (existingClient != null)
|
|
{
|
|
return BadRequest(new { message = "Client ID 已存在", clientId = dto.ClientId });
|
|
}
|
|
|
|
var clientSecret = string.IsNullOrEmpty(dto.ClientSecret) ? GenerateSecureSecret() : dto.ClientSecret;
|
|
|
|
var descriptor = new OpenIddictApplicationDescriptor
|
|
{
|
|
ClientId = dto.ClientId,
|
|
ClientSecret = clientSecret,
|
|
DisplayName = dto.DisplayName,
|
|
ClientType = string.IsNullOrEmpty(dto.ClientType) ? "confidential" : dto.ClientType,
|
|
ConsentType = string.IsNullOrEmpty(dto.ConsentType) ? "explicit" : dto.ConsentType,
|
|
};
|
|
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization);
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.EndSession);
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Introspection);
|
|
|
|
foreach (var uri in (dto.RedirectUris ?? Array.Empty<string>()).Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
|
|
{
|
|
descriptor.RedirectUris.Add(new Uri(uri));
|
|
}
|
|
|
|
foreach (var uri in (dto.PostLogoutRedirectUris ?? Array.Empty<string>()).Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
|
|
{
|
|
descriptor.PostLogoutRedirectUris.Add(new Uri(uri));
|
|
}
|
|
|
|
foreach (var grantType in dto.GrantTypes ?? Array.Empty<string>())
|
|
{
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.GrantType + grantType);
|
|
}
|
|
|
|
foreach (var scope in dto.Scopes ?? Array.Empty<string>())
|
|
{
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope);
|
|
}
|
|
|
|
var application = await _applicationManager.CreateAsync(descriptor);
|
|
var applicationId = await _applicationManager.GetIdAsync(application);
|
|
|
|
return CreatedAtAction(nameof(GetClient), new { id = applicationId }, new
|
|
{
|
|
id = applicationId,
|
|
clientId = dto.ClientId,
|
|
clientSecret = clientSecret,
|
|
displayName = dto.DisplayName,
|
|
status = "active"
|
|
});
|
|
}
|
|
|
|
[HttpPost("{id}/generate-secret")]
|
|
public async Task<ActionResult<object>> GenerateSecret(string id)
|
|
{
|
|
var application = await _applicationManager.FindByIdAsync(id);
|
|
if (application == null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
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);
|
|
|
|
var newSecret = GenerateSecureSecret();
|
|
|
|
var descriptor = new OpenIddictApplicationDescriptor
|
|
{
|
|
ClientId = clientIdValue,
|
|
ClientSecret = newSecret,
|
|
DisplayName = displayNameValue,
|
|
ClientType = clientType,
|
|
ConsentType = consentType,
|
|
};
|
|
|
|
foreach (var permission in permissions)
|
|
{
|
|
descriptor.Permissions.Add(permission);
|
|
}
|
|
|
|
foreach (var uriString in redirectUris)
|
|
{
|
|
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
|
|
{
|
|
descriptor.RedirectUris.Add(uri);
|
|
}
|
|
}
|
|
|
|
foreach (var uriString in postLogoutRedirectUris)
|
|
{
|
|
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
|
|
{
|
|
descriptor.PostLogoutRedirectUris.Add(uri);
|
|
}
|
|
}
|
|
|
|
await _applicationManager.UpdateAsync(application, descriptor);
|
|
|
|
return Ok(new
|
|
{
|
|
clientId = clientIdValue,
|
|
clientSecret = newSecret,
|
|
message = "新密钥已生成,请妥善保管,刷新后将无法再次查看"
|
|
});
|
|
}
|
|
|
|
[HttpDelete("{id}")]
|
|
public async Task<IActionResult> DeleteClient(string id)
|
|
{
|
|
var application = await _applicationManager.FindByIdAsync(id);
|
|
if (application == null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
var clientIdValue = await _applicationManager.GetClientIdAsync(application);
|
|
|
|
try
|
|
{
|
|
await _applicationManager.DeleteAsync(application);
|
|
_logger.LogInformation("Deleted OpenIddict application {ClientId}", clientIdValue);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to delete OpenIddict application for client {ClientId}", clientIdValue);
|
|
return BadRequest(new { message = ex.Message });
|
|
}
|
|
|
|
return NoContent();
|
|
}
|
|
|
|
[HttpPut("{id}")]
|
|
public async Task<IActionResult> UpdateClient(string id, [FromBody] UpdateOAuthClientDto dto)
|
|
{
|
|
var application = await _applicationManager.FindByIdAsync(id);
|
|
if (application == null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
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 redirectUris = await _applicationManager.GetRedirectUrisAsync(application);
|
|
var postLogoutRedirectUris = await _applicationManager.GetPostLogoutRedirectUrisAsync(application);
|
|
var existingPermissions = await _applicationManager.GetPermissionsAsync(application);
|
|
|
|
var descriptor = new OpenIddictApplicationDescriptor
|
|
{
|
|
ClientId = clientIdValue,
|
|
DisplayName = dto.DisplayName ?? displayNameValue,
|
|
ClientType = string.IsNullOrEmpty(dto.ClientType) ? clientType?.ToString() : dto.ClientType,
|
|
ConsentType = string.IsNullOrEmpty(dto.ConsentType) ? consentType?.ToString() : dto.ConsentType,
|
|
};
|
|
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization);
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.EndSession);
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Introspection);
|
|
|
|
if (dto.RedirectUris != null)
|
|
{
|
|
foreach (var uri in dto.RedirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
|
|
{
|
|
descriptor.RedirectUris.Add(new Uri(uri));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (var uriString in redirectUris)
|
|
{
|
|
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
|
|
{
|
|
descriptor.RedirectUris.Add(uri);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dto.PostLogoutRedirectUris != null)
|
|
{
|
|
foreach (var uri in dto.PostLogoutRedirectUris.Where(u => Uri.TryCreate(u, UriKind.Absolute, out _)))
|
|
{
|
|
descriptor.PostLogoutRedirectUris.Add(new Uri(uri));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
foreach (var uriString in postLogoutRedirectUris)
|
|
{
|
|
if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
|
|
{
|
|
descriptor.PostLogoutRedirectUris.Add(uri);
|
|
}
|
|
}
|
|
}
|
|
|
|
var grantTypes = dto.GrantTypes ?? existingPermissions
|
|
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.GrantType))
|
|
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.GrantType.Length));
|
|
|
|
var scopes = dto.Scopes ?? existingPermissions
|
|
.Where(p => p.StartsWith(OpenIddictConstants.Permissions.Prefixes.Scope))
|
|
.Select(p => p.Substring(OpenIddictConstants.Permissions.Prefixes.Scope.Length));
|
|
|
|
foreach (var grantType in grantTypes)
|
|
{
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.GrantType + grantType);
|
|
}
|
|
|
|
foreach (var scope in scopes)
|
|
{
|
|
descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope);
|
|
}
|
|
|
|
await _applicationManager.UpdateAsync(application, descriptor);
|
|
|
|
return NoContent();
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|
|
|
|
public class CreateOAuthClientDto
|
|
{
|
|
public string ClientId { get; set; } = string.Empty;
|
|
public string? ClientSecret { get; set; }
|
|
public string DisplayName { get; set; } = string.Empty;
|
|
public string[]? RedirectUris { get; set; }
|
|
public string[]? PostLogoutRedirectUris { get; set; }
|
|
public string[]? Scopes { get; set; }
|
|
public string[]? GrantTypes { get; set; }
|
|
public string? ClientType { get; set; }
|
|
public string? ConsentType { get; set; }
|
|
public string? Status { get; set; }
|
|
public string? Description { get; set; }
|
|
}
|
|
|
|
public class UpdateOAuthClientDto
|
|
{
|
|
public string? DisplayName { get; set; }
|
|
public string[]? RedirectUris { get; set; }
|
|
public string[]? PostLogoutRedirectUris { get; set; }
|
|
public string[]? Scopes { get; set; }
|
|
public string[]? GrantTypes { get; set; }
|
|
public string? ClientType { get; set; }
|
|
public string? ConsentType { get; set; }
|
|
public string? Status { get; set; }
|
|
public string? Description { get; set; }
|
|
}
|