-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Magnus Sandgren <5285192+MagnusSandgren@users.noreply.github.com>
- Loading branch information
1 parent
bde5845
commit c779eac
Showing
27 changed files
with
1,083 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
...gdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationBuilderExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} |
25 changes: 25 additions & 0 deletions
25
...igdir.Domain.Dialogporten.GraphQL/Common/Authentication/AuthenticationOptionsValidator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/JwtSchemeSelectorMiddleware.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>(); | ||
} |
44 changes: 44 additions & 0 deletions
44
src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/LocalDevelopmentUser.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
64 changes: 64 additions & 0 deletions
64
src/Digdir.Domain.Dialogporten.GraphQL/Common/Authentication/TokenIssuerCache.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AllowAnonymousHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
src/Digdir.Domain.Dialogporten.GraphQL/Common/Authorization/AuthorizationOptionsSetup.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
Oops, something went wrong.