Skip to content

Commit

Permalink
--wip-- [skip-ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
knuhau committed Sep 16, 2024
1 parent a7e769a commit cef705a
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Buffers.Text;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using NSec.Cryptography;

namespace Digdir.Domain.Dialogporten.Application.Common;
Expand All @@ -10,6 +12,8 @@ public interface ICompactJwsGenerator
{
string GetCompactJws(Dictionary<string, object?> claims);
bool VerifyCompactJws(string compactJws);
bool VerifyCompactJwsTimestamp(string compactJwt);
bool TryGetClaimValue(string compactJws, string claim, [NotNullWhen(true)] out string? value);
}

public class Ed25519Generator : ICompactJwsGenerator
Expand All @@ -22,12 +26,11 @@ public class Ed25519Generator : ICompactJwsGenerator
public Ed25519Generator(IOptions<ApplicationSettings> applicationSettings)
{
_applicationSettings = applicationSettings.Value;
InitSigningKey();
}

public string GetCompactJws(Dictionary<string, object?> claims)
{
InitSigningKey();

var header = JsonSerializer.SerializeToUtf8Bytes(new
{
alg = "EdDSA",
Expand Down Expand Up @@ -62,23 +65,80 @@ public string GetCompactJws(Dictionary<string, object?> claims)

public bool VerifyCompactJws(string compactJws)
{
var parts = compactJws.Split('.');
if (parts.Length != 3) return false;
try
{
var parts = compactJws.Split('.');
if (parts.Length != 3) return false;

var header = Base64Url.Decode(parts[0]);
var header = Base64Url.Decode(parts[0]);

var headerJson = JsonSerializer.Deserialize<JsonElement>(header);
if (headerJson.TryGetProperty("kid", out var kid))
var headerJson = JsonSerializer.Deserialize<JsonElement>(header);
if (headerJson.TryGetProperty("kid", out var kid))
{
if (kid.GetString() != _kid) return false;
}
else
{
return false;
}

var signature = Base64Url.Decode(parts[2]);
return SignatureAlgorithm.Ed25519.Verify(_publicKey!, Encoding.UTF8.GetBytes(parts[0] + '.' + parts[1]), signature);
}
catch (Exception)
{
if (kid.GetString() != _kid) return false;
// Log?
return false;
}
}

public bool VerifyCompactJwsTimestamp(string compactJwt)
{
try
{
var parts = compactJwt.Split('.');
if (parts.Length != 3) return false;

var payload = Base64Url.Decode(parts[1]);
var payloadJson = JsonSerializer.Deserialize<JsonElement>(payload);
if (!payloadJson.TryGetProperty(DialogTokenClaimTypes.Expires, out var exp)) return false;

var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
return exp.GetInt64() > now;
}
else
catch (Exception)
{
// Log?
return false;
}
}

public bool TryGetClaimValue(string compactJws, string claim, [NotNullWhen(true)] out string? value)
{
value = null;
try
{
var parts = compactJws.Split('.');
if (parts.Length != 3)
{
return false;
}

var payload = Base64Url.Decode(parts[1]);
var payloadJson = JsonSerializer.Deserialize<JsonElement>(payload);
if (!payloadJson.TryGetProperty(claim, out var claimValue))
{
return false;
}

var signature = Base64Url.Decode(parts[2]);
return SignatureAlgorithm.Ed25519.Verify(_publicKey!, Encoding.UTF8.GetBytes(parts[0] + '.' + parts[1]), signature);
value = claimValue.GetString();
return value != null;
}
catch (Exception)
{
// Log?
return false;
}
}

private void InitSigningKey()
Expand All @@ -104,6 +164,13 @@ public LocalDevelopmentCompactJwsGeneratorDecorator(ICompactJwsGenerator _)
public string GetCompactJws(Dictionary<string, object?> claims) => "local-development-jws";

public bool VerifyCompactJws(string compactJws) => true;
public bool VerifyCompactJwsTimestamp(string compactJwt) => true;

public bool TryGetClaimValue(string compactJws, string claim, [NotNullWhen(true)] out string? value)
{
value = "local-development-claim";
return true;
}
}

public static class Base64Url
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using Digdir.Domain.Dialogporten.Application.Common;
using Digdir.Domain.Dialogporten.GraphQL.Common.Extensions.HotChocolate;
using HotChocolate.Authorization;
using HotChocolate.Resolvers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using AuthorizationOptions = Microsoft.AspNetCore.Authorization.AuthorizationOptions;

namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authorization;

internal sealed class AuthorizationOptionsSetup : IConfigureOptions<AuthorizationOptions>
{
private readonly GraphQlSettings _options;
private readonly ICompactJwsGenerator _compactJwsGenerator;
private const string DialogTokenHeader = "DigDir-Dialog-Token";

public AuthorizationOptionsSetup(IOptions<GraphQlSettings> options)
public AuthorizationOptionsSetup(IOptions<GraphQlSettings> options, ICompactJwsGenerator compactJwsGenerator)
{
_options = options.Value;
_compactJwsGenerator = compactJwsGenerator ?? throw new ArgumentNullException(nameof(compactJwsGenerator));
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
}

public void Configure(AuthorizationOptions options)
Expand Down Expand Up @@ -41,5 +50,84 @@ public void Configure(AuthorizationOptions options)
options.AddPolicy(AuthorizationPolicy.Testing, builder => builder
.Combine(options.DefaultPolicy)
.RequireScope(AuthorizationScope.Testing));

options.AddPolicy("foo", policy => policy
.Combine(options.GetPolicy(AuthorizationPolicy.EndUser)!)
.RequireAssertion(context =>
{
// Cast resource to MiddleWareContext
// Get first value from FieldSelection.Arguments (DialogIdSubscriptionInput)
// Get HttpContext from context.Resource.ContextData, key is "HttpContext" and assign to var httpContext
if (context.Resource is not AuthorizationContext authContext)
{
return false;
}

if (!authContext.ContextData.TryGetValue("HttpContext", out var httpContextObj))
{
return false;
}

if (httpContextObj is not HttpContext httpContext)
{
return false;
}

if (!authContext.Document.Definitions.TryGetSubscriptionDialogId(out var dialogId))
{
return false;
}

if (!httpContext.Request.Headers.TryGetValue(DialogTokenHeader, out var dialogToken))
{
// requestBuilder.SetQuery("");
// const string message = "{\"errors\": [{\"message\": \"Forbidden, missing header 'DigDir-Dialog-Token'\"}]}";
// await SendForbiddenAsync(context, message, cancellationToken);
return false;
}

if (string.IsNullOrWhiteSpace(dialogToken))
{
// requestBuilder.SetQuery("");
// const string message = "{\"errors\": [{\"message\": \"Forbidden, empty token\"}]}";
// await SendForbiddenAsync(context, message, cancellationToken);
return false;
}

if (!_compactJwsGenerator.VerifyCompactJws(dialogToken!))
{
// requestBuilder.SetQuery("");
// const string message = "{\"errors\": [{\"message\": \"Forbidden, invalid token\"}]}";
// await SendForbiddenAsync(context, message, cancellationToken);
return false;
}

if (!_compactJwsGenerator.VerifyCompactJwsTimestamp(dialogToken!))
{
// requestBuilder.SetQuery("");
// const string message = "{\"errors\": [{\"message\": \"Forbidden, expired token\"}]}";
// await SendForbiddenAsync(context, message, cancellationToken);
return false;
}

if (!_compactJwsGenerator.TryGetClaimValue(dialogToken!, "i", out var dialogTokenDialogId))
{
// requestBuilder.SetQuery("");
// const string message = "{\"errors\": [{\"message\": \"Forbidden, missing claim 'i', (DialogId)\"}]}";
// await SendForbiddenAsync(context, message, cancellationToken);
return false;
}

if (dialogId.ToString() != dialogTokenDialogId)
{
// requestBuilder.SetQuery("");
// const string message = "{\"errors\": [{\"message\": \"Forbidden, token dialogId does not match subscription dialogId\"}]}";
// await SendForbiddenAsync(context, message, cancellationToken);
return false;
}


return true;
}));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using HotChocolate.Language;

namespace Digdir.Domain.Dialogporten.GraphQL.Common.Extensions.HotChocolate;

public static class DefinitionNodeExtensions
{
public static bool TryGetSubscriptionDialogId(this IReadOnlyList<IDefinitionNode> definitions, out Guid dialogId)
{
dialogId = Guid.Empty;

foreach (var definition in definitions)
{
if (definition is not OperationDefinitionNode operationDefinition)
{
continue;
}

if (operationDefinition.Operation != OperationType.Subscription)
{
continue;
}

if (operationDefinition.SelectionSet.Selections[0] is not FieldNode fieldNode)
{
continue;
}

var dialogIdArgument = fieldNode.Arguments.SingleOrDefault(x => x.Name.Value == "dialogId");

if (dialogIdArgument is null)
{
continue;
}

if (dialogIdArgument.Value.Value is null)
{
continue;
}

if (Guid.TryParse(dialogIdArgument.Value.Value.ToString(), out dialogId))
{
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

namespace Digdir.Domain.Dialogporten.GraphQL.EndUser.DialogById;

[Authorize(Policy = AuthorizationPolicy.EndUser)]
public sealed class Subscriptions
{
[Subscribe]
[Authorize(AuthorizationPolicy.EndUserSubscription, ApplyPolicy.Validation)]

Check failure on line 12 in src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/Subscriptions.cs

View workflow job for this annotation

GitHub Actions / build / build-and-test

'AuthorizationPolicy' does not contain a definition for 'EndUserSubscription'

Check failure on line 12 in src/Digdir.Domain.Dialogporten.GraphQL/EndUser/DialogById/Subscriptions.cs

View workflow job for this annotation

GitHub Actions / build / build-and-test

'AuthorizationPolicy' does not contain a definition for 'EndUserSubscription'
[Topic($"{Constants.DialogUpdatedTopic}{{{nameof(dialogId)}}}")]
public DialogUpdatedPayload DialogUpdated(Guid dialogId,
[EventMessage] Guid eventMessage)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
using System.Net;
using Digdir.Domain.Dialogporten.GraphQL.EndUser;
using Digdir.Domain.Dialogporten.GraphQL.EndUser.DialogById;
using Digdir.Domain.Dialogporten.GraphQL.EndUser.SearchDialogs;
using Digdir.Domain.Dialogporten.Infrastructure.Persistence;
using HotChocolate.AspNetCore;
using HotChocolate.AspNetCore.Serialization;
using HotChocolate.Execution;
using HotChocolate.Utilities;
using Microsoft.AspNetCore.Server.HttpSys;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Digdir.Domain.Dialogporten.GraphQL;

Expand All @@ -12,8 +19,8 @@ public static IServiceCollection AddDialogportenGraphQl(this IServiceCollection
return services
.AddGraphQLServer()
// This assumes that subscriptions have been set up by the infrastructure
.AddSubscriptionType<Subscriptions>()
.AddAuthorization()
.AddSubscriptionType<Subscriptions>()
.RegisterDbContext<DialogDbContext>()
.AddDiagnosticEventListener<ApplicationInsightEventListener>()
.AddQueryType<Queries>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,15 @@
}
},
"LocalDevelopment": {
"UseLocalDevelopmentUser": true,
"UseLocalDevelopmentResourceRegister": true,
"UseLocalDevelopmentOrganizationRegister": true,
"UseLocalDevelopmentNameRegister": true,
"UseLocalDevelopmentAltinnAuthorization": true,
"UseLocalDevelopmentUser": false,
"UseLocalDevelopmentResourceRegister": false,
"UseLocalDevelopmentOrganizationRegister": false,
"UseLocalDevelopmentNameRegister": false,
"UseLocalDevelopmentAltinnAuthorization": false,
"UseLocalDevelopmentCloudEventBus": true,
"UseLocalDevelopmentCompactJwsGenerator": false,
"DisableShortCircuitOutboxDispatcher": true,
"DisableCache": false,
"DisableAuth": true
"DisableCache": true,
"DisableAuth": false
}
}

0 comments on commit cef705a

Please sign in to comment.