Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ft grpc #13

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace CoreSharp.Cqrs.Grpc.AspNetCore
{
public class GrpcCqrsAspNetCoreBearerConfiguration
{
public string Secret { get; set; }

public string JwtIssuer { get; set; } = "coresharp-grpc";

public int ClockSkewSeconds { get; set; } = 30;

public bool RequireHttpsMetadata { get; set; }

public bool SaveToken { get; set; }

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks>net5.0;netcoreapp3.1</TargetFrameworks>
<IsPackable>true</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.4" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

namespace CoreSharp.Cqrs.Grpc.AspNetCore
{
public static class GrpcCqrsAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddCoreSharpGrpc(this AuthenticationBuilder builder, GrpcCqrsAspNetCoreBearerConfiguration configuration)
{
// register confiuration
builder.Services.AddSingleton(x => configuration);

builder.AddJwtBearer($"Bearer-{configuration.JwtIssuer}", x => {
x.RequireHttpsMetadata = configuration.RequireHttpsMetadata;
x.SaveToken = configuration.SaveToken;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.Default.GetBytes(configuration.Secret)),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.FromSeconds(configuration.ClockSkewSeconds)
};
});

return builder;
}

public static IApplicationBuilder UseCoreSharpAuthentication(this IApplicationBuilder builder)
{
return builder.Use(async (context, next) =>
{
var cfg = context.RequestServices.GetService<GrpcCqrsAspNetCoreBearerConfiguration>();

if (!context.User.Identity.IsAuthenticated)
{
var key = "Authorization";
var authParts = (context.Request.Headers.TryGetValue(key, out var sv) ? sv.ToString() : null)?.Split(" ");
var token = authParts?.Length > 1 ? authParts[1] : null;
string scheme = null;
if (!string.IsNullOrWhiteSpace(token))
{
try
{
var jwtToken = new JwtSecurityToken(token);
scheme = jwtToken.Issuer == cfg.JwtIssuer ? $"Bearer-{jwtToken.Issuer}" : null;
}
catch (Exception)
{
}
}

// validate against selected scheme
if (!string.IsNullOrWhiteSpace(scheme))
{
var result = await context.AuthenticateAsync(scheme);
if (result.Succeeded)
{
context.User = result.Principal;
}
}
}

// continue
await next();
});

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@ public class GrpcCqrsAspNetCoreRawConfiguration
public int TimeoutMs { get; set; } = 10000;

public IEnumerable<string> ContractsAssemblies { get; set; }

public string ServerId { get; set; }

public bool ExposeProto { get; set; } = true;

public string MapperValidator { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CoreSharp.Common.Extensions;
using System;
using CoreSharp.Common.Extensions;

namespace CoreSharp.Cqrs.Grpc.AspNetCore
{
Expand All @@ -10,7 +11,10 @@ public static GrpcCqrsAspNetCoreConfiguration ToConfiguration(this GrpcCqrsAspNe
{
ContractsAssemblies = raw.ContractsAssemblies.ToAssemblies(),
ServiceNamePrefix = raw.ServiceNamePrefix,
TimeoutMs = raw.TimeoutMs
TimeoutMs = raw.TimeoutMs,
ServerId = raw.ServerId,
ExposeProto = raw.ExposeProto,
MapperValidator = !string.IsNullOrWhiteSpace(raw.MapperValidator) ? Type.GetType(raw.MapperValidator) : null
};
return cfg;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFrameworks>net5.0;netcoreapp3.1</TargetFrameworks>
<IsPackable>true</IsPackable>
</PropertyGroup>

Expand All @@ -12,6 +12,7 @@

<ItemGroup>
<ProjectReference Include="..\CoreSharp.Cqrs.Grpc.Processors\CoreSharp.Cqrs.Grpc.Processors.csproj" />
<ProjectReference Include="..\CoreSharp.Identity.Extensions\CoreSharp.Identity.Extensions.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Linq;
using CoreSharp.Cqrs.Grpc.Aspects;
using CoreSharp.Cqrs.Grpc.Common;
using Grpc.Core;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace CoreSharp.Cqrs.Grpc.AspNetCore
{
public class GrpcClientAuthorizationForwardAspect : IGrpcClientAspect
{

private readonly IHttpContextAccessor _httpContextAccessor;

public GrpcClientAuthorizationForwardAspect(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}

public void AfterExecution(object rsp, Exception e)
{
}

public void BeforeExecution(object req)
{
}

public void OnCall(CallOptions callOptions, object channelRequest, GrpcCqrsCallOptions cqrsCallOptions)
{
// auth header passthrough
StringValues vs = default;
if ((cqrsCallOptions == null || !cqrsCallOptions.AddInternalAuthorization)
&& _httpContextAccessor?.HttpContext?.Request?.Headers?.TryGetValue("authorization", out vs) == true
&& !string.IsNullOrWhiteSpace(vs.ToString()))
{
var key = "authorization";

// remove existing header
var existingAuth = callOptions.Headers.FirstOrDefault(x => x.Key == key);
if (existingAuth != null)
{
callOptions.Headers.Remove(existingAuth);
}

// add new header
// note: avoid very long headers, it will result in an error
var authHeader = vs.ToString();
callOptions.Headers.Add(key, authHeader);
}
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,30 @@
using CoreSharp.Cqrs.Grpc.AspNetCore;
using CoreSharp.Cqrs.Grpc.Contracts;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Logging;
using SimpleInjector;

namespace Microsoft.AspNetCore.Routing
{
public static class GrpcCqrsEndpointRouteBuilderExtensions
{

public static GrpcServiceEndpointConventionBuilder MapCqrsGrpcWithAuthorization(this IEndpointRouteBuilder endpoints)
{

// get channel info
var cqrsAdapter = (CqrsContractsAdapter)endpoints.ServiceProvider.GetService(typeof(CqrsContractsAdapter));
var channelInfo = cqrsAdapter.ToCqrsChannelInfo();

// get logger for auth
ILogger logger = (endpoints.ServiceProvider.GetService(typeof(ILoggerFactory)) as ILoggerFactory)?.CreateLogger("CqrsGrpcServer");

// map and return
var map = endpoints.MapCqrsGrpc();
map = map.AddCqrsAuthorization(channelInfo, logger);
return map;
}

public static GrpcServiceEndpointConventionBuilder MapCqrsGrpc(this IEndpointRouteBuilder endpoints)
{
// map proto list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static IServiceCollection AddCqrsGrpc(this IServiceCollection services,
// register grpc
services.AddGrpc();
services.AddSingleton(svc => container.GetInstance<IServiceMethodProvider<GrpcService>>());
services.AddSingleton(svc => container.GetInstance<CqrsContractsAdapter>());

// return
return services;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Threading.Tasks;
using CoreSharp.Cqrs.Grpc.Common;
using CoreSharp.Cqrs.Resolver;
using CoreSharp.Identity.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Logging;

namespace CoreSharp.Cqrs.Grpc.AspNetCore
{
public static class GrpcServiceEndpointConventionBuilderExtensions
{

public static GrpcServiceEndpointConventionBuilder AddCqrsAuthorization(this GrpcServiceEndpointConventionBuilder builder, IEnumerable<CqrsChannelInfo> cqrs, ILogger logger = null)
{

builder.Add(cnv => {

var execDelegate = cnv.RequestDelegate;
cnv.RequestDelegate = ctx => {

// no path set, this should not happen
if(string.IsNullOrWhiteSpace(ctx.Request.Path))
{
logger?.LogWarning("Request path is not set.");
ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
}

// get path definition
var path = ctx.Request.Path.Value.ToLowerInvariant();
var definition = cqrs.Where(x => ((CqrsInfo)x).GetPath().ToLowerInvariant().StartsWith(path))
.OrderByDescending(x => ((CqrsInfo)x).GetPath().Length).FirstOrDefault();

// definition not found, this should not be the case
if(definition == null)
{
logger?.LogWarning("No CQRS found for request path.");
ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
}

// log required permissions
logger?.LogDebug("Authorization required: permissions={permissions}",
string.Join(',', definition.Permissions?.ToList() ?? new List<string>()));

// authorization required
if (definition.IsAuthorize) {

// user required for authorization
var identity = ctx.User?.Identity as ClaimsIdentity;
if (identity == null || !identity.IsAuthenticated)
{
ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
}

// log required permissions
logger?.LogDebug("Authorization user: permissions={permissions}, system={systemUser}",
string.Join(',', identity.GetPermissions()?.ToList() ?? new List<string>()),
identity.IsSystemUser());

// permissions check
if (definition.Permissions != null && definition.Permissions.Any() && !identity.IsSystemUser()
&& !identity.HasAnyPermission(definition.Permissions.ToArray()))
{
ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
}

}

// log access
logger?.LogDebug("Authorization passed for path {path}.", ctx.Request.Path);

// execute action
return execDelegate(ctx);
};

});

return builder;
}


}
}
Loading