diff --git a/README.md b/README.md index ed4c537..bf07612 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This repository contains a collection of analyzers developed during MSHack 2022, | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | MH001 | Just an easter egg analyzer. Finds instances where an integer variable is assigned to 42 and recommends renaming the identifier to `meaningOfLife`. | | MH002 | Emits a warning when a Razor component writes to one of its parameter properties directly. | +| MH003 | Recommends that the user leverage the `dotnet user-jwts` command line tool when they are using JWT-bearer based auth. | | MH005 | Finds 'out', 'in', or 'ref' modifiers on arguments in request delegates. | ## 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..017d7cf 100644 --- a/samples/WebSample/WebSample.csproj +++ b/samples/WebSample/WebSample.csproj @@ -10,4 +10,8 @@ ReferenceOutputAssembly="false" OutputItemType="Analyzer" /> + + + + diff --git a/src/Analyzers/DiagnosticDescriptors.cs b/src/Analyzers/DiagnosticDescriptors.cs index 1024dd5..697726f 100644 --- a/src/Analyzers/DiagnosticDescriptors.cs +++ b/src/Analyzers/DiagnosticDescriptors.cs @@ -20,6 +20,14 @@ public static class DiagnosticDescriptors DiagnosticSeverity.Warning, 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); + public static readonly DiagnosticDescriptor BadArgumentModifier = new( "MH005", "Found an invalid modifier on an argument", 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 index dadb5f1..e73d4da 100644 --- a/src/Analyzers/WellKnownTypes.cs +++ b/src/Analyzers/WellKnownTypes.cs @@ -25,11 +25,18 @@ public static bool TryCreate(Compilation compilation, out WellKnownTypes? wellKn return false; } + const string JwtBearerExtensions = "Microsoft.Extensions.DependencyInjection.JwtBearerExtensions"; + if (compilation.GetTypeByMetadataName(JwtBearerExtensions) is not { } jwtBearerExtensions) + { + return false; + } + wellKnownTypes = new WellKnownTypes { EndpointRouteBuilderExtensions = endpointRouteBuilderExtensions, Delegate = @delegate, IResult = iResult, + JwtBearerExtensions = jwtBearerExtensions }; return true; @@ -38,5 +45,6 @@ public static bool TryCreate(Compilation compilation, out WellKnownTypes? wellKn public INamedTypeSymbol EndpointRouteBuilderExtensions { get; private set; } = null!; public INamedTypeSymbol Delegate { get; private set; } = null!; public INamedTypeSymbol IResult { get; private set; } = null!; + public INamedTypeSymbol JwtBearerExtensions { get; private set; } = null!; } } 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 533cc74..7de1abb 100644 --- a/tests/MSHack2022.Tests.csproj +++ b/tests/MSHack2022.Tests.csproj @@ -11,6 +11,7 @@ + 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 d24e793..a36c571 100644 --- a/tests/Verifiers/CSharpAnalyzerVerifier.cs +++ b/tests/Verifiers/CSharpAnalyzerVerifier.cs @@ -62,7 +62,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);