Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ If you need to implement custom authentication login, for example validating cre
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>();
builder.Services.AddPermissions<T>();

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:

Expand All @@ -208,24 +208,51 @@ The **AddPermissions** extension method requires an implementation of the [IPerm
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.
The library provides the built-in [ScopeClaimPermissionHandler class](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication/Permissions/ScopeClaimPermissionHandler.cs), that checks for permissions reading the default **scope** claims of the current user (_scp_ or _http://schemas.microsoft.com/identity/claims/scope_). To use this default handler, we can just write this:

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:
builder.Services.AddScopePermissions();
// The line above is equivalent to builder.Services.AddPermissions<ScopeClaimPermissionHandler>();

Based on the scenario, we can provide our own implementation, for example reading different claims or using external services (database, HTTP calls, etc.) to get user permissions.

Then, just use the [PermissionAttribute](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication.Abstractions/Permissions/PermissionAttribute.cs) or the [RequirePermission](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication/PermissionAuthorizationExtensions.cs#L98) extension method:

// In a Controller
[Permissions("profile")]
[Permission("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")
.RequirePermission("profile")

With the [ScopeClaimPermissionHandler](https://github.com/marcominerva/SimpleAuthentication/blob/master/src/SimpleAuthentication/Permissions/ScopeClaimPermissionHandler.cs) mentioned above, the invocation succeeds if the user has a _scp_ or _http://schemas.microsoft.com/identity/claims/scope_ claim that contains the _profile_ value, for example:

"scp": "profile email calendar:read"

It is also possible to explicitly create a policy that requires the one or more permissions:

builder.Services.AddAuthorization(options =>
{
// Define permissions using a policy.
options.AddPolicy("UserProfile", builder => builder.RequirePermission("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:
// In a Controller
[Authorize(Policy = "UserProfile")]
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));
})
.RequireAuthorization(policyNames: "UserProfile")

"scope": "profile email calendar:read"

**Samples**

Expand Down
4 changes: 2 additions & 2 deletions samples/Controllers/ApiKeySample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,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
4 changes: 2 additions & 2 deletions samples/Controllers/BasicAuthenticationSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,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 @@ -30,7 +30,7 @@ public ActionResult<LoginResponse> Login(LoginRequest loginRequest, DateTime? ex
var claims = new List<Claim>();
if (loginRequest.Scopes?.Any() ?? false)
{
claims.Add(new("scope", loginRequest.Scopes));
claims.Add(new("scp", loginRequest.Scopes));
}

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

public record class LoginRequest(string UserName, string Password, string Scopes);
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
Expand Up @@ -12,12 +12,12 @@ namespace JwtBearerSample.Controllers;
[Produces(MediaTypeNames.Application.Json)]
public class PeopleController : ControllerBase
{
[Permissions(Permissions.PeopleRead, Permissions.PeopleAdmin)]
[Authorize(Policy = "PeopleRead")] // [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)]
[Authorize(Policy = "PeopleRead")] // [Permissions(Permissions.PeopleRead, Permissions.PeopleAdmin)]
[HttpGet("{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesDefaultResponseType]
Expand Down
33 changes: 20 additions & 13 deletions samples/Controllers/JwtBearerSample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Security.Claims;
using JwtBearerSample.Authentication;
using JwtBearerSample.Controllers;
using Microsoft.AspNetCore.Authentication;
using SimpleAuthentication;
using SimpleAuthentication.Permissions;
Expand All @@ -15,19 +16,25 @@
builder.Services.AddSimpleAuthentication(builder.Configuration);

// Enable permission-based authorization.
builder.Services.AddPermissions<ScopeClaimPermissionHandler>();
builder.Services.AddScopePermissions(); // This is equivalent to builder.Services.AddPermissions<ScopeClaimPermissionHandler>();

//builder.Services.AddAuthorization(options =>
//{
// options.FallbackPolicy = options.DefaultPolicy = new AuthorizationPolicyBuilder()
// .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
// .RequireAuthenticatedUser()
// .Build();
// Define a custom handler for permission handling.
//builder.Services.AddPermissions<CustomPermissionHandler>();

// options.AddPolicy("Bearer", policy => policy
// .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
// .RequireAuthenticatedUser());
//});
builder.Services.AddAuthorization(options =>
{
// Define permissions using a policy.
options.AddPolicy("PeopleRead", builder => builder.RequirePermission(Permissions.PeopleRead, Permissions.PeopleAdmin));

//options.FallbackPolicy = options.DefaultPolicy = new AuthorizationPolicyBuilder()
// .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
// .RequireAuthenticatedUser()
// .Build();

//options.AddPolicy("Bearer", policy => policy
// .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
// .RequireAuthenticatedUser());
});

// Uncomment the following line if you have multiple authentication schemes and
// you need to determine the authentication scheme at runtime (for example, you don't want to use the default authentication scheme).
Expand All @@ -49,13 +56,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
76 changes: 61 additions & 15 deletions samples/MinimalApis/JwtBearerSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,25 @@
builder.Services.AddSimpleAuthentication(builder.Configuration);

// Enable permission-based authorization.
builder.Services.AddPermissions<ScopeClaimPermissionHandler>();
builder.Services.AddScopePermissions(); // This is equivalent to builder.Services.AddPermissions<ScopeClaimPermissionHandler>();

//builder.Services.AddAuthorization(options =>
//{
// options.FallbackPolicy = options.DefaultPolicy = new AuthorizationPolicyBuilder()
// .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
// .RequireAuthenticatedUser()
// .Build();
// Define a custom handler for permission handling.
//builder.Services.AddPermissions<CustomPermissionHandler>();

// options.AddPolicy("Bearer", policy => policy
// .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
// .RequireAuthenticatedUser());
//});
builder.Services.AddAuthorization(options =>
{
// Define permissions using a policy.
options.AddPolicy("PeopleRead", builder => builder.RequirePermission(Permissions.PeopleRead, Permissions.PeopleAdmin));

//options.FallbackPolicy = options.DefaultPolicy = new AuthorizationPolicyBuilder()
// .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
// .RequireAuthenticatedUser()
// .Build();

//options.AddPolicy("Bearer", policy => policy
// .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
// .RequireAuthenticatedUser());
});

// Uncomment the following line if you have multiple authentication schemes and
// you need to determine the authentication scheme at runtime (for example, you don't want to use the default authentication scheme).
Expand Down Expand Up @@ -78,7 +84,7 @@
var claims = new List<Claim>();
if (loginRequest.Scopes?.Any() ?? false)
{
claims.Add(new("scope", loginRequest.Scopes));
claims.Add(new("scp", loginRequest.Scopes));
}

var token = jwtBearerService.CreateToken(loginRequest.UserName, claims, absoluteExpiration: expiration);
Expand Down Expand Up @@ -114,17 +120,57 @@
return TypedResults.Ok(new User(user.Identity!.Name));
})
.RequireAuthorization()
.RequirePermissions("profile")
.RequirePermission("profile")
.WithOpenApi(operation =>
{
operation.Description = "This endpoint requires the 'profile' permission";
return operation;
});

app.MapGet("api/people", () =>
{
return TypedResults.NoContent();
})
.RequireAuthorization(policyNames: "PeopleRead")
.WithOpenApi(operation =>
{
operation.Description = $"This endpoint requires the '{Permissions.PeopleRead}' or '{Permissions.PeopleAdmin}' permissions";
return operation;
});

app.Run();

public record class User(string? UserName);

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

public record class LoginResponse(string Token);
public record class LoginResponse(string Token);

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);
}
}

public static class Permissions
{
public const string PeopleRead = "people:read";
public const string PeopleWrite = "people:write";
public const string PeopleAdmin = "people:admin";
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ namespace SimpleAuthentication.Permissions;
/// Specifies that the class or method that this attribute is applied to requires the specified authorization based on permissions.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class PermissionsAttribute : AuthorizeAttribute
public class PermissionAttribute : AuthorizeAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="PermissionsAttribute"/> class with the specified permissions.
/// Initializes a new instance of the <see cref="PermissionAttribute"/> class with the specified permissions.
/// </summary>
/// <param name="permissions">The permission list to require for authorization.</param>
public PermissionsAttribute(params string[] permissions)
public PermissionAttribute(params string[] permissions)
: base(string.Join(",", permissions))
{
}
Expand Down
45 changes: 43 additions & 2 deletions src/SimpleAuthentication/PermissionAuthorizationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace SimpleAuthentication;
public static class PermissionAuthorizationExtensions
{
/// <summary>
/// Registers services required by permission-based authorization, using the specified <typeparamref name="T"/> implementation to validates permissions.
/// Registers services required by permission-based authorization, using the specified <typeparamref name="T"/> implementation to validate permissions.
/// </summary>
/// <typeparam name="T">The type implementing <see cref="IPermissionHandler"/> to register.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
Expand All @@ -31,6 +31,16 @@ public static IServiceCollection AddPermissions<T>(this IServiceCollection servi
return services;
}

/// <summary>
/// Registers services required by permission-based authorization, using the default <see cref="ScopeClaimPermissionHandler"/> implementation that uses scopes to validate permissions.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
/// <exception cref="ArgumentNullException">One or more required configuration settings are missing.</exception>
/// <seealso cref="ScopeClaimPermissionHandler"/>
public static IServiceCollection AddScopePermissions(this IServiceCollection services)
=> services.AddPermissions<ScopeClaimPermissionHandler>();

/// <summary>
/// Registers services required by permission-based authorization, using the specified <typeparamref name="T"/> implementation to validates permissions.
/// </summary>
Expand All @@ -46,6 +56,37 @@ public static AuthenticationBuilder AddPermissions<T>(this AuthenticationBuilder
return builder;
}

/// <summary>
/// Registers services required by permission-based authorization, using the default <see cref="ScopeClaimPermissionHandler"/> implementation that uses scopes to validate permissions.
/// </summary>
/// <param name="builder">The <see cref="AuthenticationBuilder"/> to add services to.</param>
/// <returns>An <see cref="AuthenticationBuilder"/> that can be used to further customize authentication.</returns>
/// <exception cref="ArgumentNullException">One or more required configuration settings are missing.</exception>
/// <seealso cref="IPermissionHandler"/>
/// <seealso cref="ScopeClaimPermissionHandler"/>
/// <seealso cref="AuthenticationBuilder"/>
public static AuthenticationBuilder AddScopePermissions(this AuthenticationBuilder builder)
{
builder.Services.AddPermissions<ScopeClaimPermissionHandler>();
return builder;
}

/// <summary>
/// Adds a <see cref="PermissionRequirement"/> to the <see cref="AuthorizationPolicyBuilder.Requirements"/> for this instance.
/// </summary>
/// <param name="builder">The <see cref="AuthorizationPolicyBuilder"/> to add policy to.</param>
/// <param name="permissions">The list of permissions to add.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
/// <exception cref="ArgumentNullException">One or more required configuration settings are missing.</exception>
/// <seealso cref="AuthorizationPolicyBuilder"/>
/// <seealso cref="PermissionRequirement"/>
public static AuthorizationPolicyBuilder RequirePermission(this AuthorizationPolicyBuilder builder, params string[] permissions)
{
ArgumentNullException.ThrowIfNull(builder);

return builder.AddRequirements(new PermissionRequirement(permissions));
}

/// <summary>
/// Adds permission-based authorization policy to the endpoint(s).
/// </summary>
Expand All @@ -54,7 +95,7 @@ public static AuthenticationBuilder AddPermissions<T>(this AuthenticationBuilder
/// <returns>The original <see cref="IEndpointConventionBuilder"/> parameter.</returns>
/// <exception cref="ArgumentNullException">One or more required configuration settings are missing.</exception>
/// <seealso cref="IEndpointConventionBuilder"/>
public static TBuilder RequirePermissions<TBuilder>(this TBuilder builder, params string[] permissions) where TBuilder : IEndpointConventionBuilder
public static TBuilder RequirePermission<TBuilder>(this TBuilder builder, params string[] permissions) where TBuilder : IEndpointConventionBuilder
{
ArgumentNullException.ThrowIfNull(builder);

Expand Down
Loading