diff --git a/.github/workflows/test_backend.yml b/.github/workflows/test_backend.yml index a3b4b1a85..ac63900ca 100644 --- a/.github/workflows/test_backend.yml +++ b/.github/workflows/test_backend.yml @@ -33,6 +33,8 @@ jobs: Authentication__Google__ClientSecret: ${{ secrets.Authentication__Google__ClientSecret }} Authentication__Twitter__ConsumerKey: ${{ secrets.Authentication__Twitter__ConsumerKey }} Authentication__Twitter__ConsumerSecret: ${{ secrets.Authentication__Twitter__ConsumerSecret }} + Authentication__Jwt__Certificate: ${{ secrets.Authentication__Jwt__Certificate }} + Authentication__Jwt__CertificatePassword: ${{ secrets.Authentication__Jwt__CertificatePassword }} services: postgres: diff --git a/CollAction.Tests/CollAction.Tests.csproj b/CollAction.Tests/CollAction.Tests.csproj index b0f507466..025f20d33 100644 --- a/CollAction.Tests/CollAction.Tests.csproj +++ b/CollAction.Tests/CollAction.Tests.csproj @@ -12,14 +12,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all diff --git a/CollAction.Tests/Integration/Endpoint/GraphQlTests.cs b/CollAction.Tests/Integration/Endpoint/GraphQlTests.cs index ea226855f..69722eb0a 100644 --- a/CollAction.Tests/Integration/Endpoint/GraphQlTests.cs +++ b/CollAction.Tests/Integration/Endpoint/GraphQlTests.cs @@ -1,17 +1,25 @@ -using CollAction.Models; +using CollAction.Data; +using CollAction.Models; using CollAction.Services; using CollAction.Services.Crowdactions; using CollAction.Services.Crowdactions.Models; using CollAction.Services.Email; using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; using Moq; using System; using System.Collections.Generic; using System.Globalization; +using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -23,12 +31,16 @@ namespace CollAction.Tests.Integration.Endpoint public sealed class GraphQlTests : IntegrationTestBase { private readonly ICrowdactionService crowdactionService; + private readonly ApplicationDbContext context; private readonly SeedOptions seedOptions; + private readonly IConfiguration configuration; public GraphQlTests() { crowdactionService = Scope.ServiceProvider.GetRequiredService(); + context = Scope.ServiceProvider.GetRequiredService(); seedOptions = Scope.ServiceProvider.GetRequiredService>().Value; + configuration = Scope.ServiceProvider.GetRequiredService(); } [Fact] @@ -114,6 +126,31 @@ public async Task TestAuthorization() Assert.True(response.IsSuccessStatusCode, content); result = JsonDocument.Parse(content); Assert.Throws(() => result.RootElement.GetProperty("errors")); + + // Retry call as jwt admin + var user = await context.Users.FirstAsync(); + var authSection = configuration.GetSection("Authentication"); + var jwtSection = authSection.GetSection("Jwt"); + using X509Certificate2 certificate = new(Convert.FromBase64String(jwtSection["Certificate"]), jwtSection["CertificatePassword"]); + var securityToken = new JwtSecurityToken( + "CollAction", + "CollAction", + new Claim[] + { + new Claim("sub", user.Id), + new Claim("role", "admin"), + new Claim("name", user.FullName), + }, + expires: DateTime.Now.AddDays(1), + signingCredentials: new SigningCredentials(new X509SecurityKey(certificate), "RS256")); + string httpToken = new JwtSecurityTokenHandler().WriteToken(securityToken); + httpClient.DefaultRequestHeaders.Clear(); + httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", httpToken); + response = await PerformGraphQlQuery(httpClient, QueryCrowdactions, null); + content = await response.Content.ReadAsStringAsync(); + Assert.True(response.IsSuccessStatusCode, content); + result = JsonDocument.Parse(content); + Assert.Throws(() => result.RootElement.GetProperty("errors")); } [Fact] diff --git a/CollAction/CollAction.csproj b/CollAction/CollAction.csproj index a130e3cbf..a6727d325 100644 --- a/CollAction/CollAction.csproj +++ b/CollAction/CollAction.csproj @@ -35,34 +35,35 @@ - - + + - - + + - - - - - - - + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + runtime; build; native; contentfiles; analyzers - + diff --git a/CollAction/Controllers/GraphQlController.cs b/CollAction/Controllers/GraphQlController.cs index c601ea2d9..0fc03e8ff 100644 --- a/CollAction/Controllers/GraphQlController.cs +++ b/CollAction/Controllers/GraphQlController.cs @@ -6,6 +6,7 @@ using GraphQL.Types; using GraphQL.Validation; using GraphQL.Validation.Complexity; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Caching.Memory; @@ -22,6 +23,8 @@ namespace CollAction.Controllers { [Route("graphql")] [ApiController] + [Authorize(AuthenticationSchemes = "Identity.Application,Identity.External,Identity.TwoFactorRememberMe,Identity.TwoFactorUserId,Facebook,Twitter,Google,Bearer")] + [AllowAnonymous] public sealed class GraphQlController : Controller { private readonly IDocumentExecuter executer; diff --git a/CollAction/Startup.cs b/CollAction/Startup.cs index 0c8abdb2d..cfbd626f4 100644 --- a/CollAction/Startup.cs +++ b/CollAction/Startup.cs @@ -17,8 +17,7 @@ using GraphiQl; using Hangfire; using Hangfire.PostgreSql; -using MailChimp.Net; -using MailChimp.Net.Interfaces; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; @@ -30,11 +29,13 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.IdentityModel.Tokens; using RazorLight; using RazorLight.Extensions; using Serilog; using Stripe; using System; +using System.Security.Cryptography.X509Certificates; namespace CollAction { @@ -76,24 +77,6 @@ public void ConfigureServices(IServiceCollection services) o.Cookie.SecurePolicy = CookieSecurePolicy.Always; }); - IConfigurationSection authSection = configuration.GetSection("Authentication"); - services.AddAuthentication() - .AddFacebook(options => - { - authSection.GetSection("Facebook").Bind(options); - options.Scope.Add("email"); - }) - .AddTwitter(options => - { - authSection.GetSection("Twitter").Bind(options); - options.RetrieveUserDetails = true; - }) - .AddGoogle(options => - { - authSection.GetSection("Google").Bind(options); - options.Scope.Add("email"); - }); - services.AddApplicationInsightsTelemetry(configuration); services.AddMvc() @@ -183,6 +166,42 @@ public void ConfigureServices(IServiceCollection services) options.Password.RequireNonAlphanumeric = false; options.Password.RequiredLength = 8; }).ValidateDataAnnotations(); + + IConfigurationSection authSection = configuration.GetSection("Authentication"); + var authenticationBuilder = services.AddAuthentication() + .AddFacebook(options => + { + authSection.GetSection("Facebook").Bind(options); + options.Scope.Add("email"); + }) + .AddTwitter(options => + { + authSection.GetSection("Twitter").Bind(options); + options.RetrieveUserDetails = true; + }) + .AddGoogle(options => + { + authSection.GetSection("Google").Bind(options); + options.Scope.Add("email"); + }) + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => + { + options.RequireHttpsMetadata = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ValidateIssuer = true, + ValidIssuer = "CollAction", + ValidateAudience = true, + ValidAudience = "CollAction", + RequireAudience = true, + RequireExpirationTime = true, + RequireSignedTokens = true, + }; + + var jwtSection = authSection.GetSection("Jwt"); + X509Certificate2 certificate = new(Convert.FromBase64String(jwtSection["Certificate"]), jwtSection["CertificatePassword"]); + options.TokenValidationParameters.IssuerSigningKey = new X509SecurityKey(certificate); + }); } public void Configure(IApplicationBuilder app, IHostApplicationLifetime applicationLifetime) @@ -205,6 +224,9 @@ public void Configure(IApplicationBuilder app, IHostApplicationLifetime applicat app.UseForwardedHeaders(forwardedHeaderOptions); } + app.UseAuthentication(); + app.UseAuthorization(); + if (environment.IsProduction()) { app.UseHsts(); @@ -217,8 +239,6 @@ public void Configure(IApplicationBuilder app, IHostApplicationLifetime applicat app.UseGraphiQl("/graphiql", "/graphql"); } - app.UseAuthentication(); - app.UseAuthorization(); app.UseHangfireServer(new BackgroundJobServerOptions() { WorkerCount = 1 }); app.UseEndpoints(endpoints => { diff --git a/env_collaction.template b/env_collaction.template index 12085a320..562bfc49e 100644 --- a/env_collaction.template +++ b/env_collaction.template @@ -14,6 +14,8 @@ MailChimpTestListId=1a035c45ca MailChimpNewsletterListId=1a035c45ca MailChimpKey= FromAddress=hello@collaction.org +Authentication:Jwt:Certificate= +Authentication:Jwt:CertificatePassword= Authentication:Twitter:ConsumerSecret= Authentication:Twitter:ConsumerKey= Authentication:Google:ClientSecret=