Skip to content

Commit

Permalink
feat: Add GraphQL POC (#636)
Browse files Browse the repository at this point in the history
Co-authored-by: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com>
  • Loading branch information
oskogstad and MagnusSandgren authored Apr 16, 2024
1 parent bde5845 commit c779eac
Show file tree
Hide file tree
Showing 27 changed files with 1,083 additions and 6 deletions.
7 changes: 7 additions & 0 deletions Digdir.Domain.Dialogporten.sln
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digdir.Tool.Dialogporten.Ed
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digdir.Domain.Dialogporten.WebApi.Integration.Tests", "tests\Digdir.Domain.Dialogporten.WebApi.Integration.Tests\Digdir.Domain.Dialogporten.WebApi.Integration.Tests.csproj", "{42004236-D45C-4A1F-9FF9-CF12B7388389}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Digdir.Domain.Dialogporten.GraphQL", "src\Digdir.Domain.Dialogporten.GraphQL\Digdir.Domain.Dialogporten.GraphQL.csproj", "{234FE24D-1047-4E29-A625-1EB406C37A2D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -123,6 +125,10 @@ Global
{42004236-D45C-4A1F-9FF9-CF12B7388389}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42004236-D45C-4A1F-9FF9-CF12B7388389}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42004236-D45C-4A1F-9FF9-CF12B7388389}.Release|Any CPU.Build.0 = Release|Any CPU
{234FE24D-1047-4E29-A625-1EB406C37A2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{234FE24D-1047-4E29-A625-1EB406C37A2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{234FE24D-1047-4E29-A625-1EB406C37A2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{234FE24D-1047-4E29-A625-1EB406C37A2D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -147,6 +153,7 @@ Global
{B6FE45A3-FB14-4528-9957-295AB2A00A46} = {CADB8189-4AA1-4732-844A-C41DBF3EC8B7}
{030909AA-5B61-46B4-9B74-0D2D779478FF} = {3C2C775D-F2D1-42A2-B53F-CC6D5FF59633}
{42004236-D45C-4A1F-9FF9-CF12B7388389} = {CADB8189-4AA1-4732-844A-C41DBF3EC8B7}
{234FE24D-1047-4E29-A625-1EB406C37A2D} = {320B47A0-5EB8-4B6E-8C84-90633A1849CA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B2FE67FF-7622-4AFB-AD8E-961B6A39D888}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ public DialogStatus(Values id) : base(id) { }
public enum Values
{
/// <summary>
/// Dialogen er å regne som ny. Brukes typisk for enkle meldinger som ikke krever noe
/// interaksjon, eller som et initielt steg for dialoger. Dette er default.
/// Dialogen er å regne som ny. Brukes typisk for enkle meldinger som ikke krever noe
/// interaksjon, eller som et initielt steg for dialoger. Dette er default.
/// </summary>
New = 1,

/// <summary>
/// Under arbeid. Generell status som brukes for dialogtjenester der ytterligere bruker-input er
/// Under arbeid. Generell status som brukes for dialogtjenester der ytterligere bruker-input er
/// forventet.
/// </summary>
InProgress = 2,
Expand All @@ -27,8 +27,8 @@ public enum Values
Waiting = 3,

/// <summary>
/// Dialogen er i en tilstand hvor den venter på signering. Typisk siste steg etter at all
/// utfylling er gjennomført og validert.
/// Dialogen er i en tilstand hvor den venter på signering. Typisk siste steg etter at all
/// utfylling er gjennomført og validert.
/// </summary>
Signing = 4,

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Diagnostics;

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

internal static class AuthenticationBuilderExtensions
{
public static IServiceCollection AddDialogportenAuthentication(
this IServiceCollection services,
IConfiguration configuration)
{
var jwtTokenSchemas = configuration
.GetSection(GraphQlSettings.SectionName)
.Get<GraphQlSettings>()
?.Authentication
?.JwtBearerTokenSchemas;

if (jwtTokenSchemas is null || jwtTokenSchemas.Count == 0)
// Validation should have caught this.
throw new UnreachableException();

services.AddSingleton<ITokenIssuerCache, TokenIssuerCache>();

var authenticationBuilder = services.AddAuthentication();

foreach (var schema in jwtTokenSchemas)
{
authenticationBuilder.AddJwtBearer(schema.Name, options =>
{
options.MetadataAddress = schema.WellKnown;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(2)
};

options.Events = new JwtBearerEvents
{
OnMessageReceived = async context =>
{
var expectedIssuer = await context.HttpContext
.RequestServices
.GetRequiredService<ITokenIssuerCache>()
.GetIssuerForScheme(schema.Name);

if (context.HttpContext.Items.TryGetValue(Constants.CurrentTokenIssuer, out var tokenIssuer)
&& (string?)tokenIssuer != expectedIssuer)
{
context.NoResult();
}
}
};
});
}

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Digdir.Domain.Dialogporten.GraphQL.Common.Authentication;

public sealed class AuthenticationOptions
{
public required List<JwtBearerTokenSchemasOptions> JwtBearerTokenSchemas { get; init; }
}

public sealed class JwtBearerTokenSchemasOptions
{
public required string Name { get; init; }
public required string WellKnown { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using FluentValidation;

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

internal sealed class AuthenticationOptionsValidator : AbstractValidator<AuthenticationOptions>
{
public AuthenticationOptionsValidator(
IValidator<JwtBearerTokenSchemasOptions> jwtTokenSchemaValidator)
{
RuleFor(x => x.JwtBearerTokenSchemas)
.NotEmpty()
.WithMessage("At least one JwtBearerTokenSchema must be configured");
RuleForEach(x => x.JwtBearerTokenSchemas)
.SetValidator(jwtTokenSchemaValidator);
}
}

internal sealed class JwtBearerTokenSchemasOptionsValidator : AbstractValidator<JwtBearerTokenSchemasOptions>
{
public JwtBearerTokenSchemasOptionsValidator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.WellKnown).NotEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.IdentityModel.Tokens.Jwt;

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

public class JwtSchemeSelectorMiddleware
{
private readonly RequestDelegate _next;

public JwtSchemeSelectorMiddleware(RequestDelegate next)
{
_next = next;
}

public Task InvokeAsync(HttpContext context)
{
if (!context.Request.Headers.TryGetValue(Constants.Authorization, out var authorizationHeader))
return _next(context);

var token = authorizationHeader.ToString()
.Split(' ')
.LastOrDefault();

if (string.IsNullOrEmpty(token))
return _next(context);

var handler = new JwtSecurityTokenHandler();
if (!handler.CanReadToken(token))
return _next(context);

var jwtToken = handler.ReadJwtToken(token);
context.Items[Constants.CurrentTokenIssuer] = jwtToken.Issuer;
return _next(context);
}
}

public static class JwtSchemeSelectorMiddlewareExtensions
{
public static IApplicationBuilder UseJwtSchemeSelector(this IApplicationBuilder app)
=> app.UseMiddleware<JwtSchemeSelectorMiddleware>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Collections.ObjectModel;
using System.Security.Claims;
using Digdir.Domain.Dialogporten.Application.Externals.Presentation;

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

internal sealed class LocalDevelopmentUser : IUser
{
private readonly ClaimsPrincipal _principal = new(new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, "Local Development User"),
new Claim(ClaimTypes.NameIdentifier, "local-development-user"),
new Claim("pid", "03886595947"),
new Claim("scope", string.Join(" ", AuthorizationScope.AllScopes.Value)),
new Claim("consumer",
"""
{
"authority": "iso6523-actorid-upis",
"ID": "0192:991825827"
}
""")
}));

public ClaimsPrincipal GetPrincipal() => _principal;
}


internal static class AuthorizationScope
{
public const string EndUser = "digdir:dialogporten";
public const string ServiceProvider = "digdir:dialogporten.serviceprovider";
public const string ServiceProviderSearch = "digdir:dialogporten.serviceprovider.search";
public const string Testing = "digdir:dialogporten.developer.test";

internal static readonly Lazy<IReadOnlyCollection<string>> AllScopes = new(GetAll);

private static ReadOnlyCollection<string> GetAll() =>
typeof(AuthorizationScope)
.GetFields()
.Where(x => x.IsLiteral && !x.IsInitOnly && x.DeclaringType == typeof(string))
.Select(x => (string)x.GetRawConstantValue()!)
.ToList()
.AsReadOnly();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Protocols;
using Microsoft.Extensions.Options;

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

public interface ITokenIssuerCache
{
public Task<string?> GetIssuerForScheme(string schemeName);
}

public sealed class TokenIssuerCache : ITokenIssuerCache, IDisposable
{
private readonly Dictionary<string, string> _issuerMappings = new();
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
private bool _initialized;
private readonly IReadOnlyCollection<JwtBearerTokenSchemasOptions> _jwtTokenSchemas;

public TokenIssuerCache(IOptions<GraphQlSettings> apiSettings)
{
_jwtTokenSchemas = apiSettings
.Value
.Authentication
.JwtBearerTokenSchemas
?? throw new ArgumentException("JwtBearerTokenSchemas is required.");
}

public async Task<string?> GetIssuerForScheme(string schemeName)
{
await EnsureInitializedAsync();

return _issuerMappings.TryGetValue(schemeName, out var issuer)
? issuer : null;
}

private async Task EnsureInitializedAsync()
{
if (_initialized) return;
await _initializationSemaphore.WaitAsync();
if (_initialized) return;

try
{
foreach (var schema in _jwtTokenSchemas)
{
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
schema.WellKnown, new OpenIdConnectConfigurationRetriever());
var config = await configManager.GetConfigurationAsync();
_issuerMappings[schema.Name] = config.Issuer;
}

_initialized = true;
}
finally
{
_initializationSemaphore.Release();
}
}

public void Dispose()
{
_initializationSemaphore.Dispose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Authorization;

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

/// <summary>
/// This authorisation handler will bypass all requirements
/// </summary>
public class AllowAnonymousHandler : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
foreach (var requirement in context.PendingRequirements)
{
//Simply pass all requirements
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;

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

internal sealed class AuthorizationOptionsSetup : IConfigureOptions<AuthorizationOptions>
{
private readonly GraphQlSettings _options;

public AuthorizationOptionsSetup(IOptions<GraphQlSettings> options)
{
_options = options.Value;
}

public void Configure(AuthorizationOptions options)
{
var authenticationSchemas = _options
.Authentication
.JwtBearerTokenSchemas
.Select(x => x.Name)
.ToArray();

options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(authenticationSchemas)
.RequireValidConsumerClaim()
.Build();

options.AddPolicy(AuthorizationPolicy.EndUser, builder => builder
.Combine(options.DefaultPolicy)
.RequireScope(AuthorizationScope.EndUser));

options.AddPolicy(AuthorizationPolicy.ServiceProvider, builder => builder
.Combine(options.DefaultPolicy)
.RequireScope(AuthorizationScope.ServiceProvider));

options.AddPolicy(AuthorizationPolicy.ServiceProviderSearch, builder => builder
.Combine(options.DefaultPolicy)
.RequireScope(AuthorizationScope.ServiceProviderSearch));

options.AddPolicy(AuthorizationPolicy.Testing, builder => builder
.Combine(options.DefaultPolicy)
.RequireScope(AuthorizationScope.Testing));
}
}
Loading

0 comments on commit c779eac

Please sign in to comment.