Skip to content

Commit

Permalink
Merge pull request #2329 from sky1045/feature/jwt
Browse files Browse the repository at this point in the history
Introduce JWT Authentication for GraphQL(Optional)
  • Loading branch information
sky1045 authored Dec 6, 2023
2 parents 3b91364 + a9887a7 commit 68761cc
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 8 deletions.
5 changes: 5 additions & 0 deletions NineChronicles.Headless.Executable/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,10 @@
"ManagementTimeMinutes": 60,
"TxIntervalMinutes": 60,
"ThresholdCount": 29
},
"Jwt": {
"EnableJwtAuthentication": false,
"Key": "secretKey",
"Issuer": "planetariumhq.com"
}
}
34 changes: 27 additions & 7 deletions NineChronicles.Headless/GraphQLService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using NineChronicles.Headless.GraphTypes;
using NineChronicles.Headless.Middleware;
using NineChronicles.Headless.Properties;
Expand All @@ -25,6 +24,8 @@ public class GraphQLService
{
public const string LocalPolicyKey = "LocalPolicy";

public const string JwtPolicyKey = "JwtPolicy";

public const string NoCorsPolicyName = "AllowAllOrigins";

public const string SecretTokenKey = "secret";
Expand Down Expand Up @@ -129,6 +130,13 @@ public void ConfigureServices(IServiceCollection services)
services.Configure<MultiAccountManagerProperties>(Configuration.GetSection("MultiAccountManaging"));
}

var jwtOptions = Configuration.GetSection("Jwt");
if (Convert.ToBoolean(jwtOptions["EnableJwtAuthentication"]))
{
services.Configure<JwtOptions>(jwtOptions);
services.AddTransient<JwtAuthenticationMiddleware>();
}

if (!(Configuration[NoCorsKey] is null))
{
services.AddCors(
Expand Down Expand Up @@ -161,12 +169,19 @@ public void ConfigureServices(IServiceCollection services)
.AddLibplanetExplorer()
.AddUserContextBuilder<UserContextBuilder>()
.AddGraphQLAuthorization(
options => options.AddPolicy(
LocalPolicyKey,
p =>
p.RequireClaim(
"role",
"Admin")));
options =>
{
options.AddPolicy(
LocalPolicyKey,
p =>
p.RequireClaim(
"role",
"Admin"));
options.AddPolicy(
JwtPolicyKey,
p =>
p.RequireClaim("iss", jwtOptions["Issuer"]));
});
services.AddGraphTypes();
}

Expand All @@ -190,6 +205,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseMiddleware<HttpCaptureMiddleware>();

app.UseMiddleware<LocalAuthenticationMiddleware>();
if (Convert.ToBoolean(Configuration.GetSection("Jwt")["EnableJwtAuthentication"]))
{
app.UseMiddleware<JwtAuthenticationMiddleware>();
}

if (Configuration[NoCorsKey] is null)
{
app.UseCors();
Expand Down
4 changes: 4 additions & 0 deletions NineChronicles.Headless/GraphTypes/StandaloneMutation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ IConfiguration configuration
{
this.AuthorizeWith(GraphQLService.LocalPolicyKey);
}
else if (Convert.ToBoolean(configuration.GetSection("Jwt")["EnableJwtAuthentication"]))
{
this.AuthorizeWith(GraphQLService.JwtPolicyKey);
}

Field<KeyStoreMutation>(
name: "keyStore",
Expand Down
4 changes: 4 additions & 0 deletions NineChronicles.Headless/GraphTypes/StandaloneQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public class StandaloneQuery : ObjectGraphType
public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration configuration, ActionEvaluationPublisher publisher, StateMemoryCache stateMemoryCache)
{
bool useSecretToken = configuration[GraphQLService.SecretTokenKey] is { };
if (Convert.ToBoolean(configuration.GetSection("Jwt")["EnableJwtAuthentication"]))
{
this.AuthorizeWith(GraphQLService.JwtPolicyKey);
}

Field<NonNullGraphType<StateQuery>>(name: "stateQuery", arguments: new QueryArguments(
new QueryArgument<ByteStringType>
Expand Down
11 changes: 10 additions & 1 deletion NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
using Libplanet.Blockchain;
using Libplanet.Store;
using Libplanet.Types.Tx;
using Microsoft.Extensions.Configuration;
using Serilog;

namespace NineChronicles.Headless.GraphTypes
Expand Down Expand Up @@ -129,9 +130,17 @@ public PreloadStateType()

private StandaloneContext StandaloneContext { get; }

public StandaloneSubscription(StandaloneContext standaloneContext)
private IConfiguration Configuration { get; }

public StandaloneSubscription(StandaloneContext standaloneContext, IConfiguration configuration)
{
StandaloneContext = standaloneContext;
Configuration = configuration;
if (Convert.ToBoolean(configuration.GetSection("Jwt")["EnableJwtAuthentication"]))
{
this.AuthorizeWith(GraphQLService.JwtPolicyKey);
}

AddField(new EventStreamFieldType
{
Name = "tipChanged",
Expand Down
85 changes: 85 additions & 0 deletions NineChronicles.Headless/Middleware/JwtAuthenticationMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Serilog;

namespace NineChronicles.Headless.Middleware;

public class JwtAuthenticationMiddleware : IMiddleware
{
private readonly ILogger _logger;
private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
private readonly TokenValidationParameters _validationParams;

public JwtAuthenticationMiddleware(IConfiguration configuration)
{
_logger = Log.Logger.ForContext<JwtAuthenticationMiddleware>();
var jwtConfig = configuration.GetSection("Jwt");
var issuer = jwtConfig["Issuer"] ?? "";
var key = jwtConfig["Key"] ?? "";
_validationParams = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(key.PadRight(512 / 8, '\0')))
};
}

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
context.Request.Headers.TryGetValue("Authorization", out var authorization);
if (authorization.Count > 0)
{
try
{
var (scheme, token) = ExtractSchemeAndToken(authorization);
if (scheme == "Bearer")
{
ValidateTokenAndAddClaims(context, token);
}
}
catch (Exception e)
{
_logger.Error($"Authorization error {e.Message}");
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
JsonConvert.SerializeObject(
new { errpr = e.Message }
));
return;
}
}
await next(context);
}

private (string scheme, string token) ExtractSchemeAndToken(StringValues authorizationHeader)
{
var headerValues = authorizationHeader[0].Split(" ");
if (headerValues.Length < 2)
{
throw new ArgumentException("Invalid Authorization header format.");
}

return (headerValues[0], headerValues[1]);
}

private void ValidateTokenAndAddClaims(HttpContext context, string token)
{
_tokenHandler.ValidateToken(token, _validationParams, out SecurityToken validatedToken);
var jwt = (JwtSecurityToken)validatedToken;
var claims = jwt.Claims.Select(claim => new Claim(claim.Type, claim.Value));
context.User.AddIdentity(new ClaimsIdentity(claims));
}
}
1 change: 1 addition & 0 deletions NineChronicles.Headless/NineChronicles.Headless.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<PackageReference Include="MagicOnion.Abstractions" Version="5.0.3" />
<PackageReference Include="MagicOnion.Server" Version="5.0.3" />
<PackageReference Include="MagicOnion.Server.HttpGateway" Version="5.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="7.0.11" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NRedisStack" Version="0.9.0" />
Expand Down
10 changes: 10 additions & 0 deletions NineChronicles.Headless/Properties/JwtOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace NineChronicles.Headless.Properties;

public class JwtOptions
{
public bool EnableJwtAuthentication { get; }

public string Key { get; } = "";

public string Issuer { get; } = "planetariumhq.com";
}

0 comments on commit 68761cc

Please sign in to comment.