Skip to content

Commit

Permalink
Add Net8JwtBearerSample project and update OpenAPI config
Browse files Browse the repository at this point in the history
Updated SimpleAuthentication.sln to include Net8JwtBearerSample.
Replaced Swashbuckle with OpenAPI in JwtBearerSample.csproj.
Refactored Program.cs for OpenAPI configuration.
Simplified OpenApiSecurityRequirement and OpenApiResponse in Helpers.cs.
Renamed SwaggerExtensions to OpenApiExtensions in OpenApiExtensions.cs.
Added appsettings.Development.json and appsettings.json for config.
Added Net8JwtBearerSample.csproj with necessary dependencies.
Added Program.cs for Net8JwtBearerSample setup.
Added ApplicationAuthenticationSchemeProvider.cs for custom auth logic.
Added ClaimsTransformer.cs for custom claims transformation.
Added launchSettings.json for JwtBearerSample project.
  • Loading branch information
marcominerva committed Dec 11, 2024
1 parent 9ba1a77 commit ff576f1
Show file tree
Hide file tree
Showing 13 changed files with 383 additions and 61 deletions.
7 changes: 7 additions & 0 deletions SimpleAuthentication.sln
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JwtBearerSample", "samples\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleAuthentication.Swashbuckle", "src\SimpleAuthentication.Swashbuckle\SimpleAuthentication.Swashbuckle.csproj", "{2B13BC55-EA62-47F4-9857-968DED3488CA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Net8JwtBearerSample", "samples\MinimalApis\Net8JwtBearerSample\Net8JwtBearerSample.csproj", "{C987694E-87DA-44E1-BED0-CE155B2301BA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -75,6 +77,10 @@ Global
{2B13BC55-EA62-47F4-9857-968DED3488CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B13BC55-EA62-47F4-9857-968DED3488CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B13BC55-EA62-47F4-9857-968DED3488CA}.Release|Any CPU.Build.0 = Release|Any CPU
{C987694E-87DA-44E1-BED0-CE155B2301BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C987694E-87DA-44E1-BED0-CE155B2301BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C987694E-87DA-44E1-BED0-CE155B2301BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C987694E-87DA-44E1-BED0-CE155B2301BA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -88,6 +94,7 @@ Global
{D46FF74A-2392-46D7-B35D-725D3B15ED21} = {01B848F6-9AB2-41F8-985A-46C62E0EBFCF}
{8C0B7F84-A413-4370-86C9-96F97CB3616D} = {01B848F6-9AB2-41F8-985A-46C62E0EBFCF}
{14A175D5-1AA7-4B31-809F-BB47014E9B20} = {01B848F6-9AB2-41F8-985A-46C62E0EBFCF}
{C987694E-87DA-44E1-BED0-CE155B2301BA} = {01B848F6-9AB2-41F8-985A-46C62E0EBFCF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {02046913-4C76-4691-912A-75826EC29E5C}
Expand Down
3 changes: 1 addition & 2 deletions samples/MinimalApis/JwtBearerSample/JwtBearerSample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="7.2.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\SimpleAuthentication.Swashbuckle\SimpleAuthentication.Swashbuckle.csproj" />
<ProjectReference Include="..\..\..\src\SimpleAuthentication\SimpleAuthentication.csproj" />
</ItemGroup>

Expand Down
33 changes: 11 additions & 22 deletions samples/MinimalApis/JwtBearerSample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,7 @@

builder.Services.AddTransient<IClaimsTransformation, ClaimsTransformer>();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();

builder.Services.AddSwaggerGen(options =>
builder.Services.AddOpenApi(options =>
{
options.AddSimpleAuthentication(builder.Configuration);
});
Expand All @@ -63,8 +60,12 @@

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.MapOpenApi();

app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", builder.Environment.ApplicationName);
});
}

app.UseAuthentication();
Expand All @@ -86,11 +87,7 @@
var token = await jwtBearerService.CreateTokenAsync(loginRequest.UserName, claims, absoluteExpiration: expiration);
return TypedResults.Ok(new LoginResponse(token));
})
.WithOpenApi(operation =>
{
operation.Description = "Insert permissions in the scope property (for example: 'profile people:admin')";
return operation;
});
.WithDescription("Insert permissions in the scope property (for example: 'profile people:admin')");

authApiGroup.MapPost("validate", async Task<Results<Ok<User>, BadRequest>> (string token, bool validateLifetime, IJwtBearerService jwtBearerService) =>
{
Expand All @@ -103,7 +100,7 @@

return TypedResults.Ok(new User(result.Principal.Identity!.Name));
})
.WithOpenApi();
.ProducesProblem(StatusCodes.Status400BadRequest);

authApiGroup.MapPost("refresh", async (string token, bool validateLifetime, DateTime? expiration, IJwtBearerService jwtBearerService) =>
{
Expand All @@ -118,22 +115,14 @@
})
.RequireAuthorization()
.RequirePermission("profile")
.WithOpenApi(operation =>
{
operation.Description = "This endpoint requires the 'profile' permission";
return operation;
});
.WithDescription("This endpoint requires the 'profile' permission");

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;
});
.WithDescription($"This endpoint requires the '{Permissions.PeopleRead}' or '{Permissions.PeopleAdmin}' permissions");

app.Run();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using SimpleAuthentication.ApiKey;
using SimpleAuthentication.JwtBearer;

namespace JwtBearerSample.Authentication;

public class ApplicationAuthenticationSchemeProvider(IHttpContextAccessor httpContextAccessor, IOptions<AuthenticationOptions> options,
IOptions<JwtBearerSettings> jwtBearerSettingsOptions, IOptions<ApiKeySettings> apiKeySettingsOptions) : AuthenticationSchemeProvider(options)
{
private readonly JwtBearerSettings jwtBearerSettings = jwtBearerSettingsOptions.Value;
private readonly ApiKeySettings apiKeySettings = apiKeySettingsOptions.Value;

private async Task<AuthenticationScheme?> GetRequestSchemeAsync()
{
var request = (httpContextAccessor.HttpContext?.Request) ?? throw new ArgumentNullException("The HTTP request cannot be retrieved.");

// For API requests, use Jwt Bearer Authentication.
if (request.IsApiRequest())
{
return await GetSchemeAsync(jwtBearerSettings.SchemeName);
}

// For Services requests, use Api Key Authentication.
if (request.IsServiceRequest())
{
return await GetSchemeAsync(apiKeySettings.SchemeName);
}

// For the other requests, return null to let the base methods
// decide what's the best scheme based on the default schemes
// configured in the global authentication options.
return null;
}

public override async Task<AuthenticationScheme?> GetDefaultAuthenticateSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultAuthenticateSchemeAsync();

public override async Task<AuthenticationScheme?> GetDefaultChallengeSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultChallengeSchemeAsync();

public override async Task<AuthenticationScheme?> GetDefaultForbidSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultForbidSchemeAsync();

public override async Task<AuthenticationScheme?> GetDefaultSignInSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultSignInSchemeAsync();

public override async Task<AuthenticationScheme?> GetDefaultSignOutSchemeAsync() =>
await GetRequestSchemeAsync() ??
await base.GetDefaultSignOutSchemeAsync();
}

public static class HttpRequestExtensions
{
public static bool IsApiRequest(this HttpRequest request)
=> request.Path.StartsWithSegments("/api");

public static bool IsServiceRequest(this HttpRequest request)
=> request.Path.StartsWithSegments("/service");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;

namespace JwtBearerSample.Authentication;

public class ClaimsTransformer : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = principal.Identity as ClaimsIdentity;
var newClaim = new Claim(ClaimTypes.Version, "v1");
identity!.AddClaim(newClaim);

return Task.FromResult(principal);
}
}
19 changes: 19 additions & 0 deletions samples/MinimalApis/Net8JwtBearerSample/Net8JwtBearerSample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\SimpleAuthentication.Swashbuckle\SimpleAuthentication.Swashbuckle.csproj" />
<ProjectReference Include="..\..\..\src\SimpleAuthentication\SimpleAuthentication.csproj" />
</ItemGroup>

</Project>
173 changes: 173 additions & 0 deletions samples/MinimalApis/Net8JwtBearerSample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using System.Security.Claims;
using JwtBearerSample.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http.HttpResults;
using SimpleAuthentication;
using SimpleAuthentication.JwtBearer;
using SimpleAuthentication.Permissions;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddHttpContextAccessor();
builder.Services.AddProblemDetails();

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

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

// Define a custom handler for permission handling.
//builder.Services.AddPermissions<CustomPermissionHandler>();

builder.Services.AddAuthorizationBuilder()
// Define permissions using a policy.
.AddPolicy("PeopleRead", builder => builder.RequirePermission(Permissions.PeopleRead, Permissions.PeopleAdmin))
//.AddPolicy("Bearer", builder => builder.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser())
//.SetDefaultPolicy(new AuthorizationPolicyBuilder()
// .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
// .RequireAuthenticatedUser()
// .Build())
//.SetFallbackPolicy(new AuthorizationPolicyBuilder()
// .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
// .RequireAuthenticatedUser()
// .Build())
;

// 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).
//builder.Services.AddSingleton<IAuthenticationSchemeProvider, ApplicationAuthenticationSchemeProvider>();

builder.Services.AddTransient<IClaimsTransformation, ClaimsTransformer>();

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();

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

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseHttpsRedirection();

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

app.UseStatusCodePages();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseAuthentication();
app.UseAuthorization();

var authApiGroup = app.MapGroup("api/auth");

authApiGroup.MapPost("login", async (LoginRequest loginRequest, DateTime? expiration, IJwtBearerService jwtBearerService) =>
{
// Check for login rights...

// Add custom claims (optional).
var claims = new List<Claim>();
if (!string.IsNullOrWhiteSpace(loginRequest.Scopes))
{
claims.Add(new("scp", loginRequest.Scopes));
}

var token = await jwtBearerService.CreateTokenAsync(loginRequest.UserName, claims, absoluteExpiration: expiration);
return TypedResults.Ok(new LoginResponse(token));
})
.WithOpenApi(operation =>
{
operation.Description = "Insert permissions in the scope property (for example: 'profile people:admin')";
return operation;
});

authApiGroup.MapPost("validate", async Task<Results<Ok<User>, BadRequest>> (string token, bool validateLifetime, IJwtBearerService jwtBearerService) =>
{
var result = await jwtBearerService.TryValidateTokenAsync(token, validateLifetime);

if (!result.IsValid)
{
return TypedResults.BadRequest();
}

return TypedResults.Ok(new User(result.Principal.Identity!.Name));
})
.WithOpenApi();

authApiGroup.MapPost("refresh", async (string token, bool validateLifetime, DateTime? expiration, IJwtBearerService jwtBearerService) =>
{
var newToken = await jwtBearerService.RefreshTokenAsync(token, validateLifetime, expiration);
return TypedResults.Ok(new LoginResponse(newToken));
})
.WithOpenApi();

app.MapGet("api/me", (ClaimsPrincipal user) =>
{
return TypedResults.Ok(new User(user.Identity!.Name));
})
.RequireAuthorization()
.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 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";
}
Loading

0 comments on commit ff576f1

Please sign in to comment.