Skip to content

Commit 1b813a2

Browse files
authored
Merge pull request #80 from marcominerva/develop
Add permisison-based authorization
2 parents 6aaf638 + dfab3b2 commit 1b813a2

31 files changed

Lines changed: 536 additions & 186 deletions

.github/workflows/publish.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ on:
99
env:
1010
NET_VERSION: '7.x'
1111
PROJECT_NAME: src/SimpleAuthentication
12-
PROJECT_FILE: SimpleAuthentication.csproj
12+
PROJECT_FILE: SimpleAuthentication.csproj
13+
RELEASE_NAME: SimpleAuthenticationTools
1314

1415
jobs:
1516
build:
@@ -46,6 +47,6 @@ jobs:
4647
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
4748
with:
4849
tag_name: v${{ steps.nbgv.outputs.NuGetPackageVersion }}
49-
release_name: Release ${{ steps.nbgv.outputs.NuGetPackageVersion }}
50+
release_name: ${{ env.RELEASE_NAME }} ${{ steps.nbgv.outputs.NuGetPackageVersion }}
5051
draft: false
5152
prerelease: false

.github/workflows/publish_abstractions.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ on:
99
env:
1010
NET_VERSION: '7.x'
1111
PROJECT_NAME: src/SimpleAuthentication.Abstractions
12-
PROJECT_FILE: SimpleAuthentication.Abstractions.csproj
12+
PROJECT_FILE: SimpleAuthentication.Abstractions.csproj
13+
TAG_NAME: abstractions
14+
RELEASE_NAME: SimpleAuthenticationTools.Abstractions
1315

1416
jobs:
1517
build:
@@ -45,7 +47,7 @@ jobs:
4547
env:
4648
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
4749
with:
48-
tag_name: abstractions_v${{ steps.nbgv.outputs.NuGetPackageVersion }}
49-
release_name: Release Abstractions ${{ steps.nbgv.outputs.NuGetPackageVersion }}
50+
tag_name: ${{ env.TAG_NAME }}_v${{ steps.nbgv.outputs.NuGetPackageVersion }}
51+
release_name: ${{ env.RELEASE_NAME }} ${{ steps.nbgv.outputs.NuGetPackageVersion }}
5052
draft: false
5153
prerelease: false

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,39 @@ If you need to implement custom authentication login, for example validating cre
194194
}
195195
}
196196

197+
**Permission-based authorization**
198+
199+
The library provides services for adding permission-based authorization to an ASP.NET Core project. Just use the following registration at startup:
200+
201+
// Enable permission-based authorization.
202+
builder.Services.AddPermissions<ScopeClaimPermissionHandler>();
203+
204+
The **AddPermissions** extension method requires an implementation of the [IPermissionHandler interface](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/Permissions/IPermissionHandler.cs), that is responsible to check if the user owns the required permissions:
205+
206+
public interface IPermissionHandler
207+
{
208+
Task<bool> IsGrantedAsync(ClaimsPrincipal user, IEnumerable<string> permissions);
209+
}
210+
211+
In the sample above, we're using the built-in [ScopeClaimPermissionHandler class](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication/Permissions/ScopeClaimPermissionHandler.cs), that checks for permissions reading the _scope_ claim of the current user. Based on your scenario, you can provide your own implementation, for example reading different claims or using external services (database, HTTP calls, etc.) to get user permissions.
212+
213+
Then, just use the [PermissionsAttribute](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/Permissions/PermissionsAttribute.cs) or the [RequirePermissions](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication/PermissionAuthorizationExtensions.cs#L57) extension method:
214+
215+
// In a Controller
216+
[Permissions("profile")]
217+
public ActionResult<User> Get() => new User(User.Identity!.Name);
218+
219+
// In a Minimal API
220+
app.MapGet("api/me", (ClaimsPrincipal user) =>
221+
{
222+
return TypedResults.Ok(new User(user.Identity!.Name));
223+
})
224+
.RequirePermissions("profile")
225+
226+
With the [ScopeClaimPermissionHandler](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication/Permissions/ScopeClaimPermissionHandler.cs) mentioned above, this invocation succeeds if the user has a _scope_ claim that contains the _profile_ value, for example:
227+
228+
"scope": "profile email calendar:read"
229+
197230
**Samples**
198231

199232
- JWT Bearer ([Controller](https://github.com/marcominerva/SimpleAuthentication/tree/master/samples/Controllers/JwtBearerSample) | [Minimal API](https://github.com/marcominerva/SimpleAuthentication/tree/master/samples/MinimalApis/JwtBearerSample))

samples/Controllers/ApiKeySample/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
builder.Services.AddControllers();
1111
builder.Services.AddProblemDetails();
1212

13+
// Add authentication services.
1314
builder.Services.AddSimpleAuthentication(builder.Configuration);
1415

1516
//builder.Services.AddAuthorization(options =>

samples/Controllers/BasicAuthenticationSample/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
builder.Services.AddControllers();
1212
builder.Services.AddProblemDetails();
1313

14+
// Add authentication services.
1415
builder.Services.AddSimpleAuthentication(builder.Configuration);
1516

1617
//builder.Services.AddAuthorization(options =>

samples/Controllers/JwtBearerSample/Controllers/IdentityController.cs renamed to samples/Controllers/JwtBearerSample/Controllers/AuthController.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Security.Claims;
33
using Microsoft.AspNetCore.Mvc;
44
using SimpleAuthentication.JwtBearer;
5+
using Swashbuckle.AspNetCore.Annotations;
56

67
namespace JwtBearerSample.Controllers;
78

@@ -20,16 +21,17 @@ public AuthController(IJwtBearerService jwtBearerService)
2021
[HttpPost("login")]
2122
[ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)]
2223
[ProducesDefaultResponseType]
24+
[SwaggerOperation(description: "Insert permissions in the scope property (for example: 'profile people:admin')")]
2325
public ActionResult<LoginResponse> Login(LoginRequest loginRequest, DateTime? expiration = null)
2426
{
2527
// Check for login rights...
2628

2729
// Add custom claims (optional).
28-
var claims = new List<Claim>
30+
var claims = new List<Claim>();
31+
if (loginRequest.Scopes?.Any() ?? false)
2932
{
30-
new(ClaimTypes.GivenName, "Marco"),
31-
new(ClaimTypes.Surname, "Minerva")
32-
};
33+
claims.Add(new("scope", loginRequest.Scopes));
34+
}
3335

3436
var token = jwtBearerService.CreateToken(loginRequest.UserName, claims, absoluteExpiration: expiration);
3537
return new LoginResponse(token);
@@ -60,6 +62,6 @@ public ActionResult<LoginResponse> Refresh(string token, bool validateLifetime =
6062
}
6163
}
6264

63-
public record class LoginRequest(string UserName, string Password);
65+
public record class LoginRequest(string UserName, string Password, string Scopes);
6466

6567
public record class LoginResponse(string Token);

samples/Controllers/JwtBearerSample/Controllers/MeController.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System.Net.Mime;
22
using Microsoft.AspNetCore.Authorization;
33
using Microsoft.AspNetCore.Mvc;
4+
using SimpleAuthentication.Permissions;
5+
using Swashbuckle.AspNetCore.Annotations;
46

57
namespace JwtBearerSample.Controllers;
68

@@ -10,9 +12,11 @@ namespace JwtBearerSample.Controllers;
1012
public class MeController : ControllerBase
1113
{
1214
[Authorize]
15+
[Permissions("profile")]
1316
[HttpGet]
1417
[ProducesResponseType(typeof(User), StatusCodes.Status200OK)]
1518
[ProducesDefaultResponseType]
19+
[SwaggerOperation(description: "This endpoint requires the 'profile' permission")]
1620
public ActionResult<User> Get()
1721
=> new User(User.Identity!.Name);
1822
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System.Net.Mime;
2+
using Microsoft.AspNetCore.Authorization;
3+
using Microsoft.AspNetCore.Mvc;
4+
using SimpleAuthentication.Permissions;
5+
using Swashbuckle.AspNetCore.Annotations;
6+
7+
namespace JwtBearerSample.Controllers;
8+
9+
[Authorize]
10+
[ApiController]
11+
[Route("api/[controller]")]
12+
[Produces(MediaTypeNames.Application.Json)]
13+
public class PeopleController : ControllerBase
14+
{
15+
[Permissions(Permissions.PeopleRead, Permissions.PeopleAdmin)]
16+
[HttpGet]
17+
[SwaggerOperation(description: $"This endpoint requires the '{Permissions.PeopleRead}' or '{Permissions.PeopleAdmin}' permissions")]
18+
public IActionResult GetList() => NoContent();
19+
20+
[Permissions(Permissions.PeopleRead, Permissions.PeopleAdmin)]
21+
[HttpGet("{id:int}")]
22+
[ProducesResponseType(StatusCodes.Status204NoContent)]
23+
[ProducesDefaultResponseType]
24+
[SwaggerOperation(description: $"This endpoint requires the '{Permissions.PeopleRead}' or '{Permissions.PeopleAdmin}' permissions")]
25+
public IActionResult GetPerson(int id) => NoContent();
26+
27+
[Permissions(Permissions.PeopleWrite)]
28+
[HttpPost]
29+
[ProducesResponseType(StatusCodes.Status204NoContent)]
30+
[ProducesDefaultResponseType]
31+
[SwaggerOperation(description: $"This endpoint requires the '{Permissions.PeopleWrite}' permission")]
32+
public IActionResult Insert() => NoContent();
33+
34+
[Permissions(Permissions.PeopleWrite)]
35+
[HttpPut]
36+
[ProducesResponseType(StatusCodes.Status204NoContent)]
37+
[ProducesDefaultResponseType]
38+
[SwaggerOperation(description: $"This endpoint requires the '{Permissions.PeopleWrite}' permission")]
39+
public IActionResult Update() => NoContent();
40+
41+
[Permissions(Permissions.PeopleAdmin)]
42+
[HttpDelete("{id:int}")]
43+
[ProducesResponseType(StatusCodes.Status204NoContent)]
44+
[ProducesDefaultResponseType]
45+
[SwaggerOperation(description: $"This endpoint requires the '{Permissions.PeopleAdmin}' permission")]
46+
public IActionResult Delete(int id) => NoContent();
47+
}
48+
49+
public static class Permissions
50+
{
51+
public const string PeopleRead = "people:read";
52+
public const string PeopleWrite = "people:write";
53+
public const string PeopleAdmin = "people:admin";
54+
}

samples/Controllers/JwtBearerSample/JwtBearerSample.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
11+
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
1112
</ItemGroup>
1213

1314
<ItemGroup>

samples/Controllers/JwtBearerSample/Program.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
using System.Security.Claims;
12
using JwtBearerSample.Authentication;
23
using Microsoft.AspNetCore.Authentication;
34
using SimpleAuthentication;
5+
using SimpleAuthentication.Permissions;
46

57
var builder = WebApplication.CreateBuilder(args);
68

@@ -9,8 +11,12 @@
911
builder.Services.AddControllers();
1012
builder.Services.AddProblemDetails();
1113

14+
// Add authentication services.
1215
builder.Services.AddSimpleAuthentication(builder.Configuration);
1316

17+
// Enable permission-based authorization.
18+
builder.Services.AddPermissions<ScopeClaimPermissionHandler>();
19+
1420
//builder.Services.AddAuthorization(options =>
1521
//{
1622
// options.FallbackPolicy = options.DefaultPolicy = new AuthorizationPolicyBuilder()
@@ -34,6 +40,7 @@
3440

3541
builder.Services.AddSwaggerGen(options =>
3642
{
43+
options.EnableAnnotations();
3744
options.AddSimpleAuthentication(builder.Configuration);
3845
});
3946

@@ -60,4 +67,26 @@
6067

6168
app.MapControllers();
6269

63-
app.Run();
70+
app.Run();
71+
72+
public class CustomPermissionHandler : IPermissionHandler
73+
{
74+
public Task<bool> IsGrantedAsync(ClaimsPrincipal user, IEnumerable<string> permissions)
75+
{
76+
bool isGranted;
77+
78+
if (!permissions?.Any() ?? true)
79+
{
80+
isGranted = true;
81+
}
82+
else
83+
{
84+
var permissionClaim = user.FindFirstValue("permissions");
85+
var userPermissions = permissionClaim?.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? Enumerable.Empty<string>();
86+
87+
isGranted = userPermissions.Intersect(permissions!).Any();
88+
}
89+
90+
return Task.FromResult(isGranted);
91+
}
92+
}

0 commit comments

Comments
 (0)