From ed445578af186a7ad927124e7d9a9e2923a6fd11 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 19 Sep 2022 14:45:53 -0700 Subject: [PATCH 1/3] Add analyzer recommendation for user-jwts --- README.md | 1 + samples/WebSample/Program.cs | 3 + samples/WebSample/WebSample.csproj | 1 + src/Analyzers/DiagnosticDescriptors.cs | 8 + .../MinimalAuthenticationAnalyzer.cs | 43 ++++++ src/Analyzers/WellKnownTypes.cs | 26 ++++ tests/EasterEggAnalyzerTest.cs | 2 +- tests/MSHack2022.Tests.csproj | 2 + tests/MinimalNet7AnalyzerTests.cs | 141 ++++++++++++++++++ tests/Verifiers/CSharpAnalyzerVerifier.cs | 27 +++- 10 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 src/Analyzers/MinimalAuthenticationAnalyzer.cs create mode 100644 src/Analyzers/WellKnownTypes.cs create mode 100644 tests/MinimalNet7AnalyzerTests.cs diff --git a/README.md b/README.md index f4b1d3c..7da49d6 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ This repository contains a collection of analyzers developed during MSHack 2022, | Diagnostic ID | Decription | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | MH001 | Just an easter egg analyzer. Finds instances where an integer variable is assigned to 42 and recommends renaming the identifier to `meaningOfLife`. | +| MH003 | Recommends that the user leverage the `dotnet user-jwts` command line tool when they are using JWT-bearer based auth. | ## Development Instructions diff --git a/samples/WebSample/Program.cs b/samples/WebSample/Program.cs index 76f6eb7..f0868ef 100644 --- a/samples/WebSample/Program.cs +++ b/samples/WebSample/Program.cs @@ -1,4 +1,7 @@ var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication().AddJwtBearer(); + var app = builder.Build(); app.MapGet("/mh001", () => diff --git a/samples/WebSample/WebSample.csproj b/samples/WebSample/WebSample.csproj index c733029..ce80f8c 100644 --- a/samples/WebSample/WebSample.csproj +++ b/samples/WebSample/WebSample.csproj @@ -9,5 +9,6 @@ + diff --git a/src/Analyzers/DiagnosticDescriptors.cs b/src/Analyzers/DiagnosticDescriptors.cs index cd9f515..e248c0d 100644 --- a/src/Analyzers/DiagnosticDescriptors.cs +++ b/src/Analyzers/DiagnosticDescriptors.cs @@ -11,4 +11,12 @@ public static class DiagnosticDescriptors "Usage", DiagnosticSeverity.Info, isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor UseDotnetUserJwtsTool = new( + "MH003", + "Recommend using dotnet user-jwts tool", + "It looks like you're using JWT-bearer based authentication in your application. Consider using the `dotnet user-jwts` tool to generate tokens for local development.", + "Usage", + DiagnosticSeverity.Info, + isEnabledByDefault: true); } \ No newline at end of file diff --git a/src/Analyzers/MinimalAuthenticationAnalyzer.cs b/src/Analyzers/MinimalAuthenticationAnalyzer.cs new file mode 100644 index 0000000..1c358ee --- /dev/null +++ b/src/Analyzers/MinimalAuthenticationAnalyzer.cs @@ -0,0 +1,43 @@ +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.CSharp; +using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace MSHack2022.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public partial class MinimalNet7Analyzers : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + DiagnosticDescriptors.UseDotnetUserJwtsTool); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterOperationAction(static context => + { + var compilation = context.Compilation; + if (!WellKnownTypes.TryCreate(compilation, out var wellKnownTypes)) + { + Debug.Fail("One or more types could not be found. This usually means you are bad at spelling C# type names."); + return; + } + + var invocation = (IInvocationOperation)context.Operation; + var invocationTarget = invocation.TargetMethod; + if (invocationTarget is not null + && invocationTarget.Name == "AddJwtBearer" + && SymbolEqualityComparer.Default.Equals(wellKnownTypes!.JwtBearerExtensions, invocationTarget.ContainingType)) + { + context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.UseDotnetUserJwtsTool, invocation.Syntax.GetLocation())); + } + + }, OperationKind.Invocation); + } +} + diff --git a/src/Analyzers/WellKnownTypes.cs b/src/Analyzers/WellKnownTypes.cs new file mode 100644 index 0000000..9847de1 --- /dev/null +++ b/src/Analyzers/WellKnownTypes.cs @@ -0,0 +1,26 @@ +using Microsoft.CodeAnalysis; + +namespace MSHack2022.Analyzers; + +internal sealed class WellKnownTypes +{ + public static bool TryCreate(Compilation compilation, out WellKnownTypes? wellKnownTypes) + { + wellKnownTypes = default; + const string JwtBearerExtensions = "Microsoft.Extensions.DependencyInjection.JwtBearerExtensions"; + if (compilation.GetTypeByMetadataName(JwtBearerExtensions) is not { } jwtBearerExtensions) + { + return false; + } + + wellKnownTypes = new WellKnownTypes() + { + JwtBearerExtensions = jwtBearerExtensions + }; + + return true; + } + + public INamedTypeSymbol? JwtBearerExtensions { get; private set; } + +} diff --git a/tests/EasterEggAnalyzerTest.cs b/tests/EasterEggAnalyzerTest.cs index 5438195..d0b0511 100644 --- a/tests/EasterEggAnalyzerTest.cs +++ b/tests/EasterEggAnalyzerTest.cs @@ -6,7 +6,7 @@ namespace MSHack2022.Tests; -public partial class EasterEggAnalyzerTest +public class EasterEggAnalyzerTest { [Fact] public async Task TriggersOnIntWith42() diff --git a/tests/MSHack2022.Tests.csproj b/tests/MSHack2022.Tests.csproj index b410eb3..14f2c3a 100644 --- a/tests/MSHack2022.Tests.csproj +++ b/tests/MSHack2022.Tests.csproj @@ -7,6 +7,8 @@ + + diff --git a/tests/MinimalNet7AnalyzerTests.cs b/tests/MinimalNet7AnalyzerTests.cs new file mode 100644 index 0000000..11d1a27 --- /dev/null +++ b/tests/MinimalNet7AnalyzerTests.cs @@ -0,0 +1,141 @@ +using System.Threading.Tasks; +using Xunit; +using VerifyCS = MSHack2022.Tests.CSharpAnalyzerVerifier; + +namespace MSHack2022.Tests; + +public class MinimalNet7AnalyzerTests +{ + [Fact] + public async Task TriggersOnAddJwtBearerCall() + { + await VerifyCS.VerifyAnalyzerAsync(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; + +class Program +{ + static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + [|builder.Services.AddAuthentication().AddJwtBearer()|]; + + var app = builder.Build(); + + app.Run(); + } +}"); + } + + [Fact] + public async Task TriggersOnAddJwtBearerCall_TopLevelStatements() + { + await VerifyCS.VerifyAnalyzerAsync(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +[|builder.Services.AddAuthentication().AddJwtBearer()|]; + +var app = builder.Build(); + +app.Run(); +"); + } + + [Fact] + public async Task DoesNotTriggerOIncorrectCall_NonExtensionMethodInvocation() + { + await VerifyCS.VerifyAnalyzerAsync(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +AddJwtBearer(builder.Services); + +var app = builder.Build(); + +app.Run(); + +static IServiceCollection AddJwtBearer(IServiceCollection services) +{ + return services; +} +"); + } + + [Fact] + public async Task DoesNotTriggerOnIncorrectCall_ExtensionMethodInvocation() + { + await VerifyCS.VerifyAnalyzerAsync(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; + +public static class Program +{ + static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddJwtBearer(); + + var app = builder.Build(); + + app.Run(); + } +} + +public static class TestExtensionMethods +{ + public static IServiceCollection AddJwtBearer(this IServiceCollection services) + { + return services; + } +} +"); + } + + [Fact] + public async Task TriggersOnAddJwtBearerCall_InAnotherMethod() + { + await VerifyCS.VerifyAnalyzerAsync(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; + +public static class Program +{ + static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddJwtBearer(); + + var app = builder.Build(); + + app.Run(); + } +} + +public static class TestExtensionMethods +{ + public static IServiceCollection AddJwtBearer(this IServiceCollection services) + { + [|services.AddAuthentication().AddJwtBearer()|]; + return services; + } +} +"); + } +} + diff --git a/tests/Verifiers/CSharpAnalyzerVerifier.cs b/tests/Verifiers/CSharpAnalyzerVerifier.cs index 71407ef..aba5df4 100644 --- a/tests/Verifiers/CSharpAnalyzerVerifier.cs +++ b/tests/Verifiers/CSharpAnalyzerVerifier.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Testing.Verifiers; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; @@ -32,10 +33,34 @@ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticRes // We need to set the output type to an exe to properly // support top-level programs in the tests. Otherwise, // the test infra will assume we are trying to build a library. - TestState = { OutputKind = OutputKind.ConsoleApplication } + TestState = { OutputKind = OutputKind.ConsoleApplication }, + ReferenceAssemblies = GetAssemblyReferences() }; test.ExpectedDiagnostics.AddRange(expected); await test.RunAsync(CancellationToken.None); } + + private static ReferenceAssemblies GetAssemblyReferences() + { + return ReferenceAssemblies.Net.Net60.AddAssemblies(ImmutableArray.Create( + TrimAssemblyExtension(typeof(Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.Hosting.IHostBuilder).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.Hosting.HostingHostBuilderExtensions).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.AspNetCore.Builder.ConfigureHostBuilder).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.AspNetCore.Hosting.HostingAbstractionsWebHostBuilderExtensions).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.Logging.ILoggingBuilder).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.Logging.ConsoleLoggerExtensions).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.DependencyInjection.IServiceCollection).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.DependencyInjection.AntiforgeryServiceCollectionExtensions).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.FileProviders.IFileProvider).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.Configuration.ConfigurationManager).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.Configuration.JsonConfigurationExtensions).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.Configuration.IConfigurationBuilder).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.Configuration.EnvironmentVariablesExtensions).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.DependencyInjection.JwtBearerExtensions).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions).Assembly.Location))); + static string TrimAssemblyExtension(string fullPath) => fullPath.Replace(".dll", string.Empty); + } } \ No newline at end of file From e0b60d7b829711ef62916001f9bafb14eaa2664f Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 19 Sep 2022 15:01:11 -0700 Subject: [PATCH 2/3] Fix up reference assemblies --- tests/MSHack2022.Tests.csproj | 1 - tests/Verifiers/CSharpAnalyzerVerifier.cs | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/MSHack2022.Tests.csproj b/tests/MSHack2022.Tests.csproj index 53564d4..7de1abb 100644 --- a/tests/MSHack2022.Tests.csproj +++ b/tests/MSHack2022.Tests.csproj @@ -11,7 +11,6 @@ - diff --git a/tests/Verifiers/CSharpAnalyzerVerifier.cs b/tests/Verifiers/CSharpAnalyzerVerifier.cs index 75dbc51..8cc51b1 100644 --- a/tests/Verifiers/CSharpAnalyzerVerifier.cs +++ b/tests/Verifiers/CSharpAnalyzerVerifier.cs @@ -60,7 +60,9 @@ internal static ReferenceAssemblies GetReferenceAssemblies() TrimAssemblyExtension(typeof(Microsoft.Extensions.Configuration.ConfigurationManager).Assembly.Location), TrimAssemblyExtension(typeof(Microsoft.Extensions.Configuration.JsonConfigurationExtensions).Assembly.Location), TrimAssemblyExtension(typeof(Microsoft.Extensions.Configuration.IConfigurationBuilder).Assembly.Location), - TrimAssemblyExtension(typeof(Microsoft.Extensions.Configuration.EnvironmentVariablesExtensions).Assembly.Location))); + TrimAssemblyExtension(typeof(Microsoft.Extensions.Configuration.EnvironmentVariablesExtensions).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.DependencyInjection.JwtBearerExtensions).Assembly.Location), + TrimAssemblyExtension(typeof(Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions).Assembly.Location))); } static string TrimAssemblyExtension(string fullPath) => fullPath.Replace(".dll", string.Empty); From 80d931d5fb770ed65aab7a4da353bd908b6e8284 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 19 Sep 2022 15:09:09 -0700 Subject: [PATCH 3/3] Fix up project file --- samples/WebSample/WebSample.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/samples/WebSample/WebSample.csproj b/samples/WebSample/WebSample.csproj index ce80f8c..017d7cf 100644 --- a/samples/WebSample/WebSample.csproj +++ b/samples/WebSample/WebSample.csproj @@ -9,6 +9,9 @@ + + +