Skip to content

Commit

Permalink
Merge pull request #80 from marcominerva/develop
Browse files Browse the repository at this point in the history
Add permisison-based authorization
  • Loading branch information
marcominerva authored Apr 4, 2023
2 parents 6aaf638 + dfab3b2 commit 1b813a2
Show file tree
Hide file tree
Showing 31 changed files with 536 additions and 186 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ on:
env:
NET_VERSION: '7.x'
PROJECT_NAME: src/SimpleAuthentication
PROJECT_FILE: SimpleAuthentication.csproj
PROJECT_FILE: SimpleAuthentication.csproj
RELEASE_NAME: SimpleAuthenticationTools

jobs:
build:
Expand Down Expand Up @@ -46,6 +47,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
tag_name: v${{ steps.nbgv.outputs.NuGetPackageVersion }}
release_name: Release ${{ steps.nbgv.outputs.NuGetPackageVersion }}
release_name: ${{ env.RELEASE_NAME }} ${{ steps.nbgv.outputs.NuGetPackageVersion }}
draft: false
prerelease: false
8 changes: 5 additions & 3 deletions .github/workflows/publish_abstractions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ on:
env:
NET_VERSION: '7.x'
PROJECT_NAME: src/SimpleAuthentication.Abstractions
PROJECT_FILE: SimpleAuthentication.Abstractions.csproj
PROJECT_FILE: SimpleAuthentication.Abstractions.csproj
TAG_NAME: abstractions
RELEASE_NAME: SimpleAuthenticationTools.Abstractions

jobs:
build:
Expand Down Expand Up @@ -45,7 +47,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
tag_name: abstractions_v${{ steps.nbgv.outputs.NuGetPackageVersion }}
release_name: Release Abstractions ${{ steps.nbgv.outputs.NuGetPackageVersion }}
tag_name: ${{ env.TAG_NAME }}_v${{ steps.nbgv.outputs.NuGetPackageVersion }}
release_name: ${{ env.RELEASE_NAME }} ${{ steps.nbgv.outputs.NuGetPackageVersion }}
draft: false
prerelease: false
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,39 @@ If you need to implement custom authentication login, for example validating cre
}
}

**Permission-based authorization**

The library provides services for adding permission-based authorization to an ASP.NET Core project. Just use the following registration at startup:

// Enable permission-based authorization.
builder.Services.AddPermissions<ScopeClaimPermissionHandler>();

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:

public interface IPermissionHandler
{
Task<bool> IsGrantedAsync(ClaimsPrincipal user, IEnumerable<string> permissions);
}

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.

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:

// In a Controller
[Permissions("profile")]
public ActionResult<User> Get() => new User(User.Identity!.Name);

// In a Minimal API
app.MapGet("api/me", (ClaimsPrincipal user) =>
{
return TypedResults.Ok(new User(user.Identity!.Name));
})
.RequirePermissions("profile")

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:

"scope": "profile email calendar:read"

**Samples**

- 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))
Expand Down
1 change: 1 addition & 0 deletions samples/Controllers/ApiKeySample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
builder.Services.AddControllers();
builder.Services.AddProblemDetails();

// Add authentication services.
builder.Services.AddSimpleAuthentication(builder.Configuration);

//builder.Services.AddAuthorization(options =>
Expand Down
1 change: 1 addition & 0 deletions samples/Controllers/BasicAuthenticationSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
builder.Services.AddControllers();
builder.Services.AddProblemDetails();

// Add authentication services.
builder.Services.AddSimpleAuthentication(builder.Configuration);

//builder.Services.AddAuthorization(options =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc;
using SimpleAuthentication.JwtBearer;
using Swashbuckle.AspNetCore.Annotations;

namespace JwtBearerSample.Controllers;

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

// Add custom claims (optional).
var claims = new List<Claim>
var claims = new List<Claim>();
if (loginRequest.Scopes?.Any() ?? false)
{
new(ClaimTypes.GivenName, "Marco"),
new(ClaimTypes.Surname, "Minerva")
};
claims.Add(new("scope", loginRequest.Scopes));
}

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

public record class LoginRequest(string UserName, string Password);
public record class LoginRequest(string UserName, string Password, string Scopes);

public record class LoginResponse(string Token);
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Net.Mime;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SimpleAuthentication.Permissions;
using Swashbuckle.AspNetCore.Annotations;

namespace JwtBearerSample.Controllers;

Expand All @@ -10,9 +12,11 @@ namespace JwtBearerSample.Controllers;
public class MeController : ControllerBase
{
[Authorize]
[Permissions("profile")]
[HttpGet]
[ProducesResponseType(typeof(User), StatusCodes.Status200OK)]
[ProducesDefaultResponseType]
[SwaggerOperation(description: "This endpoint requires the 'profile' permission")]
public ActionResult<User> Get()
=> new User(User.Identity!.Name);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Net.Mime;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SimpleAuthentication.Permissions;
using Swashbuckle.AspNetCore.Annotations;

namespace JwtBearerSample.Controllers;

[Authorize]
[ApiController]
[Route("api/[controller]")]
[Produces(MediaTypeNames.Application.Json)]
public class PeopleController : ControllerBase
{
[Permissions(Permissions.PeopleRead, Permissions.PeopleAdmin)]
[HttpGet]
[SwaggerOperation(description: $"This endpoint requires the '{Permissions.PeopleRead}' or '{Permissions.PeopleAdmin}' permissions")]
public IActionResult GetList() => NoContent();

[Permissions(Permissions.PeopleRead, Permissions.PeopleAdmin)]
[HttpGet("{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesDefaultResponseType]
[SwaggerOperation(description: $"This endpoint requires the '{Permissions.PeopleRead}' or '{Permissions.PeopleAdmin}' permissions")]
public IActionResult GetPerson(int id) => NoContent();

[Permissions(Permissions.PeopleWrite)]
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesDefaultResponseType]
[SwaggerOperation(description: $"This endpoint requires the '{Permissions.PeopleWrite}' permission")]
public IActionResult Insert() => NoContent();

[Permissions(Permissions.PeopleWrite)]
[HttpPut]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesDefaultResponseType]
[SwaggerOperation(description: $"This endpoint requires the '{Permissions.PeopleWrite}' permission")]
public IActionResult Update() => NoContent();

[Permissions(Permissions.PeopleAdmin)]
[HttpDelete("{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesDefaultResponseType]
[SwaggerOperation(description: $"This endpoint requires the '{Permissions.PeopleAdmin}' permission")]
public IActionResult Delete(int id) => NoContent();
}

public static class Permissions
{
public const string PeopleRead = "people:read";
public const string PeopleWrite = "people:write";
public const string PeopleAdmin = "people:admin";
}
1 change: 1 addition & 0 deletions samples/Controllers/JwtBearerSample/JwtBearerSample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
</ItemGroup>

<ItemGroup>
Expand Down
31 changes: 30 additions & 1 deletion samples/Controllers/JwtBearerSample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Security.Claims;
using JwtBearerSample.Authentication;
using Microsoft.AspNetCore.Authentication;
using SimpleAuthentication;
using SimpleAuthentication.Permissions;

var builder = WebApplication.CreateBuilder(args);

Expand All @@ -9,8 +11,12 @@
builder.Services.AddControllers();
builder.Services.AddProblemDetails();

// Add authentication services.
builder.Services.AddSimpleAuthentication(builder.Configuration);

// Enable permission-based authorization.
builder.Services.AddPermissions<ScopeClaimPermissionHandler>();

//builder.Services.AddAuthorization(options =>
//{
// options.FallbackPolicy = options.DefaultPolicy = new AuthorizationPolicyBuilder()
Expand All @@ -34,6 +40,7 @@

builder.Services.AddSwaggerGen(options =>
{
options.EnableAnnotations();
options.AddSimpleAuthentication(builder.Configuration);
});

Expand All @@ -60,4 +67,26 @@

app.MapControllers();

app.Run();
app.Run();

public class CustomPermissionHandler : IPermissionHandler
{
public Task<bool> IsGrantedAsync(ClaimsPrincipal user, IEnumerable<string> permissions)
{
bool isGranted;

if (!permissions?.Any() ?? true)
{
isGranted = true;
}
else
{
var permissionClaim = user.FindFirstValue("permissions");
var userPermissions = permissionClaim?.Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? Enumerable.Empty<string>();

isGranted = userPermissions.Intersect(permissions!).Any();
}

return Task.FromResult(isGranted);
}
}
2 changes: 1 addition & 1 deletion samples/MinimalApis/ApiKeySample/ApiKeySample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

Expand Down
5 changes: 3 additions & 2 deletions samples/MinimalApis/ApiKeySample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
builder.Services.AddHttpContextAccessor();
builder.Services.AddProblemDetails();

// Add authentication services.
builder.Services.AddSimpleAuthentication(builder.Configuration);

//builder.Services.AddAuthorization(options =>
Expand Down Expand Up @@ -45,13 +46,13 @@
// Configure the HTTP request pipeline.
app.UseHttpsRedirection();

app.UseStatusCodePages();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
}

app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

Expand Down
5 changes: 3 additions & 2 deletions samples/MinimalApis/BasicAuthenticationSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
builder.Services.AddHttpContextAccessor();
builder.Services.AddProblemDetails();

// Add authentication services.
builder.Services.AddSimpleAuthentication(builder.Configuration);

//builder.Services.AddAuthorization(options =>
Expand Down Expand Up @@ -45,13 +46,13 @@
// Configure the HTTP request pipeline.
app.UseHttpsRedirection();

app.UseStatusCodePages();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
}

app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)

return Task.FromResult(principal);
}
}
}
2 changes: 1 addition & 1 deletion samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

Expand Down
Loading

0 comments on commit 1b813a2

Please sign in to comment.