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

[DRAFT] chore(gql): Add DialogToken requirement for subscriptions #1124

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
7 changes: 4 additions & 3 deletions docs/schema/V1/schema.verified.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,9 @@ type SeenLog {
isCurrentEndUser: Boolean!
}

type Subscriptions @authorize(policy: "enduser") {
dialogEvents(dialogId: UUID!): DialogEventPayload!
type Subscriptions {
"Requires a dialog token in the 'DigDir-Dialog-Token' header."
dialogEvents(dialogId: UUID!): DialogEventPayload! @authorize(policy: "enduserSubscription", apply: VALIDATION)
}

type Transmission {
Expand Down Expand Up @@ -361,4 +362,4 @@ scalar DateTime @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time"

scalar URL @specifiedBy(url: "https:\/\/tools.ietf.org\/html\/rfc3986")

scalar UUID @specifiedBy(url: "https:\/\/tools.ietf.org\/html\/rfc4122")
scalar UUID @specifiedBy(url: "https:\/\/tools.ietf.org\/html\/rfc4122")
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Buffers.Text;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Options;
Expand All @@ -22,12 +23,11 @@ public sealed 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 @@ -79,6 +79,7 @@ public bool VerifyCompactJws(string compactJws)

var signature = Base64Url.Decode(parts[2]);
return SignatureAlgorithm.Ed25519.Verify(_publicKey!, Encoding.UTF8.GetBytes(parts[0] + '.' + parts[1]), signature);

}

private void InitSigningKey()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using Microsoft.AspNetCore.Authorization;
using Digdir.Domain.Dialogporten.Application.Common.Extensions;
using Digdir.Domain.Dialogporten.GraphQL.Common.Extensions.HotChocolate;
using HotChocolate.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using AuthorizationOptions = Microsoft.AspNetCore.Authorization.AuthorizationOptions;

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

Expand All @@ -9,7 +13,7 @@ internal sealed class AuthorizationOptionsSetup : IConfigureOptions<Authorizatio

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

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

options.AddPolicy(AuthorizationPolicy.EndUserSubscription, policy => policy
.Combine(options.GetPolicy(AuthorizationPolicy.EndUser)!)
.RequireAssertion(context =>
{
if (context.Resource is not AuthorizationContext authContext)
{
return false;
}

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

context.User.TryGetClaimValue("dialogId", out var dialogIdClaimValue);
return dialogId.ToString() == dialogIdClaimValue;
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authorization;
internal static class AuthorizationPolicy
{
public const string EndUser = "enduser";
public const string EndUserSubscription = "enduserSubscription";
public const string ServiceProvider = "serviceprovider";
public const string ServiceProviderSearch = "serviceproviderSearch";
public const string Testing = "testing";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Digdir.Domain.Dialogporten.Application.Common;
using Microsoft.IdentityModel.Tokens;

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

public sealed class DialogTokenMiddleware
{
public const string DialogTokenHeader = "DigDir-Dialog-Token";
private readonly RequestDelegate _next;
private readonly ICompactJwsGenerator _compactJwsGenerator;

public DialogTokenMiddleware(RequestDelegate next, ICompactJwsGenerator compactJwsGenerator)
{
_next = next;
_compactJwsGenerator = compactJwsGenerator;
}

public Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue(DialogTokenHeader, out var dialogToken))
{
return _next(context);
}

var token = dialogToken.FirstOrDefault();
var tokenHandler = new JwtSecurityTokenHandler();
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
// ValidateLifetime = false,
ValidateIssuerSigningKey = false,
SignatureValidator = (token, parameters) =>
{
var jwt = new JwtSecurityToken(token);
return jwt;
},
}, out var securityToken);

var jwt = securityToken as JwtSecurityToken;
context.User.AddIdentity(new ClaimsIdentity(jwt!.Claims));

return _next(context);
}
catch (Exception e)
{
Console.WriteLine(e);
return _next(context);
}
}
}
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,11 @@

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

[Authorize(Policy = AuthorizationPolicy.EndUser)]
public sealed class Subscriptions
{
[Subscribe]
[Authorize(AuthorizationPolicy.EndUserSubscription, ApplyPolicy.Validation)]
[GraphQLDescription($"Requires a dialog token in the '{DialogTokenMiddleware.DialogTokenHeader}' header.")]
[Topic($"{Constants.DialogEventsTopic}{{{nameof(dialogId)}}}")]
public DialogEventPayload DialogEvents(Guid dialogId,
[EventMessage] DialogEventPayload eventMessage)
Expand Down
1 change: 1 addition & 0 deletions src/Digdir.Domain.Dialogporten.GraphQL/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ static void BuildAndRun(string[] args)
app.UseJwtSchemeSelector()
.UseAuthentication()
.UseAuthorization()
.UseMiddleware<DialogTokenMiddleware>()
.UseSerilogRequestLogging()
.UseAzureConfiguration();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,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 @@ -55,10 +55,11 @@
"LocalDevelopment": {
"UseLocalDevelopmentUser": true,
"UseLocalDevelopmentResourceRegister": true,
"UseLocalDevelopmentOrganizationRegister": true,
"UseLocalDevelopmentOrganizationRegister":true,
"UseLocalDevelopmentNameRegister": true,
"UseLocalDevelopmentAltinnAuthorization": true,
"UseLocalDevelopmentCloudEventBus": true,
"UseLocalDevelopmentCompactJwsGenerator": true,
"DisableShortCircuitOutboxDispatcher": true,
"DisableCache": false,
"DisableAuth": true
Expand Down
Loading