Master OAuth 2.0 authorization patterns with OpenIddict and ABP Framework including permission-based authorization, role-based access control, custom claims, and multi-tenant security. Use when implementing authentication/authorization for ABP applications.
OAuth 2.0 and authorization patterns for ABP Framework with OpenIddict.
// Domain.Shared/Permissions/{ProjectName}Permissions.cs
public static class {ProjectName}Permissions
{
public const string GroupName = "{ProjectName}";
public static class {Feature1}
{
public const string Default = GroupName + ".{Feature1}";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
public const string Export = Default + ".Export";
}
public static class {Feature2}
{
public const string Default = GroupName + ".{Feature2}";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
public const string ViewAll = Default + ".ViewAll"; // Admin only
public const string Cancel = Default + ".Cancel";
}
public static class {Feature3}
{
public const string Default = GroupName + ".{Feature3}";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
public const string Manage = Default + ".Manage";
}
}
// Application.Contracts/Permissions/{ProjectName}PermissionDefinitionProvider.cs
public class {ProjectName}PermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var mainGroup = context.AddGroup(
{ProjectName}Permissions.GroupName,
L("Permission:{ProjectName}"));
// Feature1
var feature1Permission = mainGroup.AddPermission(
{ProjectName}Permissions.{Feature1}.Default,
L("Permission:{Feature1}"));
feature1Permission.AddChild(
{ProjectName}Permissions.{Feature1}.Create,
L("Permission:{Feature1}.Create"));
feature1Permission.AddChild(
{ProjectName}Permissions.{Feature1}.Edit,
L("Permission:{Feature1}.Edit"));
feature1Permission.AddChild(
{ProjectName}Permissions.{Feature1}.Delete,
L("Permission:{Feature1}.Delete"));
feature1Permission.AddChild(
{ProjectName}Permissions.{Feature1}.Export,
L("Permission:{Feature1}.Export"));
// Feature2
var feature2Permission = mainGroup.AddPermission(
{ProjectName}Permissions.{Feature2}.Default,
L("Permission:{Feature2}"));
feature2Permission.AddChild(
{ProjectName}Permissions.{Feature2}.Create,
L("Permission:{Feature2}.Create"));
feature2Permission.AddChild(
{ProjectName}Permissions.{Feature2}.ViewAll,
L("Permission:{Feature2}.ViewAll"));
// Feature3 - restricted to admin
var feature3Permission = mainGroup.AddPermission(
{ProjectName}Permissions.{Feature3}.Default,
L("Permission:{Feature3}"),
multiTenancySide: MultiTenancySides.Host); // Host-only permission
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<{ProjectName}Resource>(name);
}
}
[Authorize({ProjectName}Permissions.{Feature}.Default)]
public class {Entity}AppService : ApplicationService, I{Entity}AppService
{
[Authorize({ProjectName}Permissions.{Feature}.Create)]
public async Task<{Entity}Dto> CreateAsync(CreateUpdate{Entity}Dto input)
{
// Only users with {Feature}.Create permission can execute
}
[Authorize({ProjectName}Permissions.{Feature}.Edit)]
public async Task<{Entity}Dto> UpdateAsync(Guid id, CreateUpdate{Entity}Dto input)
{
// Only users with {Feature}.Edit permission can execute
}
[Authorize({ProjectName}Permissions.{Feature}.Delete)]
public async Task DeleteAsync(Guid id)
{
// Only users with {Feature}.Delete permission can execute
}
[AllowAnonymous]
public async Task<{Entity}Dto> GetPublicInfoAsync(Guid id)
{
// Anyone can access
}
}
public class {Entity}AppService : ApplicationService, I{Entity}AppService
{
public async Task<{Entity}Dto> GetAsync(Guid id)
{
var entity = await _repository.GetAsync(id);
// Check if user can view this entity
if (entity.OwnerId != CurrentUser.Id &&
entity.AssignedUserId != CurrentUser.Id)
{
// User is neither the owner nor assigned - check admin permission
await AuthorizationService.CheckAsync(
{ProjectName}Permissions.{Feature}.ViewAll);
}
return ObjectMapper.Map<{Entity}, {Entity}Dto>(entity);
}
public async Task<bool> CanCancelAsync(Guid id)
{
var entity = await _repository.GetAsync(id);
// Check various conditions
if (entity.Status == {Entity}Status.Completed)
return false;
// Owner can always cancel
if (entity.OwnerId == CurrentUser.Id)
return true;
// Check cancel permission
return await AuthorizationService.IsGrantedAsync(
{ProjectName}Permissions.{Feature}.Cancel);
}
}
// Define policy
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("admin"));
options.AddPolicy("DoctorOrAdmin", policy =>
policy.RequireRole("admin", "doctor"));
options.AddPolicy("CanManagePatients", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim("Permission", ClinicPermissions.Patients.Default) ||
context.User.IsInRole("admin")));
});
}
// Use in AppService
[Authorize(Policy = "DoctorOrAdmin")]
public async Task<DoctorScheduleDto> GetScheduleAsync(Guid doctorId)
{
// ...
}
public class ClinicDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly IIdentityRoleRepository _roleRepository;
private readonly IdentityRoleManager _roleManager;
private readonly IPermissionManager _permissionManager;
public async Task SeedAsync(DataSeedContext context)
{
await CreateRoleIfNotExistsAsync("Admin");
await CreateRoleIfNotExistsAsync("Doctor");
await CreateRoleIfNotExistsAsync("Receptionist");
await AssignPermissionsToRolesAsync();
}
private async Task CreateRoleIfNotExistsAsync(string roleName)
{
var role = await _roleRepository.FindByNormalizedNameAsync(roleName.ToUpperInvariant());
if (role == null)
{
role = new IdentityRole(GuidGenerator.Create(), roleName);
await _roleManager.CreateAsync(role);
}
}
private async Task AssignPermissionsToRolesAsync()
{
// Admin gets all permissions
var allPermissions = new[]
{
ClinicPermissions.Patients.Default,
ClinicPermissions.Patients.Create,
ClinicPermissions.Patients.Edit,
ClinicPermissions.Patients.Delete,
ClinicPermissions.Appointments.Default,
ClinicPermissions.Appointments.ViewAll,
ClinicPermissions.Doctors.Default,
ClinicPermissions.Doctors.ManageSchedule
};
foreach (var permission in allPermissions)
{
await _permissionManager.SetForRoleAsync("Admin", permission, true);
}
// Doctor permissions
await _permissionManager.SetForRoleAsync("Doctor", ClinicPermissions.Appointments.Default, true);
await _permissionManager.SetForRoleAsync("Doctor", ClinicPermissions.Patients.Default, true);
// Receptionist permissions
await _permissionManager.SetForRoleAsync("Receptionist", ClinicPermissions.Patients.Create, true);
await _permissionManager.SetForRoleAsync("Receptionist", ClinicPermissions.Appointments.Create, true);
}
}
// AuthServer/ClaimsPrincipalContributor.cs
public class ClinicClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency
{
private readonly IRepository<Doctor, Guid> _doctorRepository;
public ClinicClaimsPrincipalContributor(IRepository<Doctor, Guid> doctorRepository)
{
_doctorRepository = doctorRepository;
}
public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context)
{
var identity = context.ClaimsPrincipal.Identities.FirstOrDefault();
if (identity == null) return;
var userId = identity.FindUserId();
if (!userId.HasValue) return;
// Add custom claims
var doctor = await _doctorRepository.FirstOrDefaultAsync(d => d.UserId == userId);
if (doctor != null)
{
identity.AddClaim(new Claim("doctor_id", doctor.Id.ToString()));
identity.AddClaim(new Claim("specialization", doctor.Specialization));
}
}
}
// Register in module
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpClaimsServiceOptions>(options =>
{
options.RequestedClaims.Add("doctor_id");
options.RequestedClaims.Add("specialization");
});
}
public class AppointmentAppService : ApplicationService
{
public async Task<List<AppointmentDto>> GetMyAppointmentsAsync()
{
// Get doctor_id from claims
var doctorIdClaim = CurrentUser.FindClaim("doctor_id");
if (doctorIdClaim == null)
{
throw new BusinessException("User is not a doctor");
}
var doctorId = Guid.Parse(doctorIdClaim.Value);
return await _appointmentRepository
.GetListAsync(a => a.DoctorId == doctorId);
}
}
public class TenantPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var group = context.AddGroup("TenantSettings");
// Tenant-side permission (available in tenants)
group.AddPermission(
"TenantSettings.Manage",
multiTenancySide: MultiTenancySides.Tenant);
// Host-side permission (only host/admin)
group.AddPermission(
"TenantSettings.ManageAll",
multiTenancySide: MultiTenancySides.Host);
}
}
public class CrossTenantReportService : ApplicationService
{
private readonly IDataFilter _dataFilter;
private readonly ICurrentTenant _currentTenant;
[Authorize("Reports.CrossTenant")]
public async Task<ReportDto> GenerateCrossTenantReportAsync()
{
// Verify host-level access
if (_currentTenant.Id.HasValue)
{
throw new BusinessException("Cross-tenant reports only available from host");
}
using (_dataFilter.Disable<IMultiTenant>())
{
// Access data across all tenants
var allPatients = await _patientRepository.GetCountAsync();
// ...
}
}
}
// AuthServer/AuthServerModule.cs
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<OpenIddictBuilder>(builder =>
{
builder.AddValidation(options =>
{
options.AddAudiences("ClinicManagementSystem");
options.UseLocalServer();
options.UseAspNetCore();
});
});
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpOpenIddictOptions>(options =>
{
options.AddDevelopmentEncryptionAndSigningCertificate = true;
});
// Configure OpenIddict scopes
Configure<OpenIddictServerOptions>(options =>
{
options.Scopes.Add("offline_access");
options.Scopes.Add("profile");
options.Scopes.Add("email");
options.Scopes.Add("clinic_api");
});
}
// HttpApi.Host configuration
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = configuration["AuthServer:Authority"];
options.RequireHttpsMetadata = Convert.ToBoolean(
configuration["AuthServer:RequireHttpsMetadata"]);
options.Audience = "ClinicManagementSystem";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
}
public async Task<PatientDto> GetAsync(Guid id)
{
// Validate input
if (id == Guid.Empty)
{
throw new AbpValidationException("Invalid patient ID");
}
var patient = await _patientRepository.FindAsync(id);
if (patient == null)
{
throw new EntityNotFoundException(typeof(Patient), id);
}
// Authorization check
await AuthorizationService.CheckAsync(ClinicPermissions.Patients.Default);
return ObjectMapper.Map<Patient, PatientDto>(patient);
}
[Audited] // Enable audit logging for this service
public class PatientAppService : ApplicationService
{
[DisableAuditing] // Disable for specific methods
public async Task<List<PatientDto>> GetListAsync()
{
// ...
}
}
[Authorize]For foundational patterns, see the shared knowledge base:
| Topic | File | Description | |-------|------|-------------| | Permission naming | knowledge/conventions/permissions.md | Permission format and hierarchy | | Naming conventions | knowledge/conventions/naming.md | Permission constant naming |
This skill is used by:
npx skills add thapaliyabikendra/openiddict-authorization下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer