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);