{"id":1347,"date":"2025-04-14T00:00:00","date_gmt":"2025-04-13T22:00:00","guid":{"rendered":"https:\/\/demo.sidekick.be\/itenium\/check-web-api-security\/"},"modified":"2025-11-28T13:31:41","modified_gmt":"2025-11-28T12:31:41","slug":"check-web-api-security","status":"publish","type":"post","link":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/","title":{"rendered":"UnitTest: Check Security on your Controllers"},"content":{"rendered":"<h1 id=\"tldr\">TL&amp;DR<\/h1>\n<pre><code class=\"language-csharp\">Type[] controllerTypes = typeof(YourController).Assembly.GetTypes()\n  .Where(t =&gt; t.GetCustomAttributes(typeof(ApiControllerAttribute), true).Any())\n  .ToArray();\n\nvar missingAuthActions = new List&lt;string&gt;();\nforeach (Type controller in controllerTypes)\n{\n  \/\/ If the entire Controller has Authorize\/AllowsAnonymous attribute, just move on\n  bool ctlHasAuthorize = controller.GetCustomAttributes(typeof(AuthorizeAttribute), true).Any();\n  if (ctlHasAuthorize)\n    continue;\n\n  bool ctlHasAllowAnon = controller.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Any();\n  if (ctlHasAllowAnon)\n    continue;\n\n  \/\/ NonAction methods do not represent an endpoint, and thus need no security check\n  var methods = controller\n    .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)\n    .Where(m =&gt; !m.GetCustomAttributes(typeof(NonActionAttribute), true).Any());\n\n  foreach (MethodInfo method in methods)\n  {\n    bool hasAuthorize = method.GetCustomAttributes(typeof(AuthorizeAttribute), true).Any();\n    bool hasAllowAnonymous = method.GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Any();\n    if (!hasAllowAnonymous &amp;&amp; !hasAuthorize)\n    {\n      missingAuthActions.Add($\"{controller.Name}.{method.Name}\");\n    }\n  }\n}\n\nif (missingAuthActions.Any())\n{\n  var sortedMethods = string.Join(\"n\", missingAuthActions.OrderBy(x =&gt; x));\n  Assert.Fail($\"The following actions are missing [Authorize] attribute:n{sortedMethods}\");\n}\n<\/code><\/pre>\n<h1 id=\"variations\">Variations<\/h1>\n<p>Maybe you want to remove the code allowing <code>[AllowAnonymous]<\/code> or something\u2026<\/p>\n<h2 id=\"finding-the-controllers\">Finding the Controllers<\/h2>\n<p>The code above checks for the presence of <code>[ApiController]<\/code> but your mileage may vary:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Controllers have [ApiController] attribute\n.Where(t =&gt; t.GetCustomAttributes(typeof(ApiControllerAttribute), true).Any())\n\n\/\/ Controllers inherit from ControllerBase\n.Where(t =&gt; t.IsSubclassOf(typeof(ControllerBase)))\n\n\/\/ Check for \"Controller\" in the name (or use EndsWith)\n.Where(t =&gt; t.Name.Contains(\"Controller\"))\n<\/code><\/pre>\n<h2 id=\"notimplementedexception\">NotImplementedException<\/h2>\n<p>In a blatant violation of the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Interface_segregation_principle\">Interface Segregation Principle (ISP)<\/a> some of our controllers implemented an interface but only implemented a few of the methods, with all others just doing a:<\/p>\n<pre><code class=\"language-csharp\">throw new NotImplementedException();\n<\/code><\/pre>\n<p>And of course this broke the test \ud83d\ude48<\/p>\n<p>More AI to the rescue! Thank god for AI too, because I\u2019m pretty sure I would\u2019ve spent a lot of time on this otherwise \ud83d\udd25<\/p>\n<p>This doesn\u2019t catch all variations (ex: <code>var ex = new NotImplementedException(); throw ex;<\/code>) but was good enough for our code-base.<\/p>\n<pre><code class=\"language-csharp\">public static bool MethodThrowsNotImplementedException(MethodInfo method)\n{\n  byte[]? methodBody = method.GetMethodBody()?.GetILAsByteArray();\n  if (methodBody == null)\n    return false;\n\n  for (int i = 0; i &lt; methodBody.Length - 5; i++)\n  {\n    \/\/ Look for NEWOBJ (0x73) followed later by THROW (0x7A)\n    if (methodBody[i] == 0x73 &amp;&amp; methodBody[i + 5] == 0x7A)\n    {\n      var ctorToken = BitConverter.ToInt32(methodBody, i + 1);\n      var ctor = method.Module.ResolveMethod(ctorToken) as ConstructorInfo;\n      if (ctor?.DeclaringType == typeof(NotImplementedException))\n        return true;\n    }\n  }\n  return false;\n}\n<\/code><\/pre>\n<h1 id=\"minimal-apis\">Minimal APIs<\/h1>\n<p>We\u2019re not using minimal APIs but for the sake of completeness, here is the code to check the same thing:<\/p>\n<h2 id=\"programcs\">Program.cs<\/h2>\n<p>Here we need to check if either <code>RequireAuthorization<\/code> or <code>AllowAnonymous<\/code> fluent methods were called for each endpoint.<\/p>\n<p>Also note the <code>public partial class Program<\/code> addition so that we can reference Program from the test. This is probably not necessary if you have nicely split your startup code instead of just dumping everything in Program.cs \ud83d\ude09<\/p>\n<pre><code class=\"language-csharp\">app.MapGet(\"\/unsecured\", () =&gt; \"Stormy\");\napp.MapGet(\"\/secured\", () =&gt; \"Sunny\").RequireAuthorization();\napp.MapGet(\"\/anonymous\", () =&gt; \"Cloudy\").AllowAnonymous();\n\napp.Run();\n\n\/\/ Enable access to Program from the test\n#if DEBUG\npublic partial class Program { }\n#endif\n<\/code><\/pre>\n<h2 id=\"the-test\">The Test<\/h2>\n<pre><code class=\"language-csharp\">var factory = new WebApplicationFactory&lt;Program&gt;().WithWebHostBuilder(builder =&gt;\n{\n  builder.ConfigureTestServices(services =&gt; { \/* Mock services as needed *\/ });\n});\n\nvar endpointDataSource = factory.Services.GetRequiredService&lt;EndpointDataSource&gt;();\nvar unsecured = new List&lt;string&gt;();\nforeach (var endpoint in endpointDataSource.Endpoints)\n{\n  bool isAuthorized = endpoint.Metadata.GetMetadata&lt;IAuthorizeData&gt;() != null;\n  bool isAnonymousAllowed = endpoint.Metadata.GetMetadata&lt;IAllowAnonymous&gt;() != null;\n  if (!isAuthorized &amp;&amp; !isAnonymousAllowed)\n  {\n    unsecured.Add(endpoint.DisplayName ?? \"??\");\n  }\n}\n\nif (unsecured.Any())\n{\n  var lst = string.Join(\"n\", unsecured);\n  Assert.Fail($\"Missing RequireAuthorization\/AllowAnonymous for endpoints:n{lst}\");\n}\n<\/code><\/pre>\n<h1>Extras<\/h1>\n<p><b>Stuff that came into being during the making of this post<\/b><\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/itenium-be\/DotNet-Controller-Security-Check\">Source code, variations and tests for the tests<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Adding the <code>[AllowAnonymous]<\/code> or <code>[Authorize(\"policy\")]<\/code> whenever a new Controller Action Method is added, it\u2019s something that is easily forgotten.<\/p>\n<p>\u2026 As I noticed when I was looking at some of our controllers \ud83d\ude35<\/p>\n<p>Of course I have written this test many times before. These days it can be quickly re-created with AI but to avoid having to debug its code, here is the copy pasta version, with some possible variations.<\/p>\n","protected":false},"author":1,"featured_media":1959,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[11],"tags":[21,10],"class_list":["post-1347","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","tag-testing","tag-war-story"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.5 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>UnitTest: Check Security on your Controllers - Itenium<\/title>\n<meta name=\"robots\" content=\"noindex, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"UnitTest: Check Security on your Controllers - Itenium\" \/>\n<meta property=\"og:description\" content=\"Adding the [AllowAnonymous] or [Authorize(&quot;policy&quot;)] whenever a new Controller Action Method is added, it\u2019s something that is easily forgotten.\u2026 As I noticed when I was looking at some of our controllers \ud83d\ude35Of course I have written this test many times before. These days it can be quickly re-created with AI but to avoid having to debug its code, here is the copy pasta version, with some possible variations.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/\" \/>\n<meta property=\"og:site_name\" content=\"Itenium\" \/>\n<meta property=\"article:published_time\" content=\"2025-04-13T22:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-11-28T12:31:41+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/controller-security.png\" \/>\n\t<meta property=\"og:image:width\" content=\"360\" \/>\n\t<meta property=\"og:image:height\" content=\"300\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Usdevelop\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Usdevelop\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"3 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/\",\"url\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/\",\"name\":\"UnitTest: Check Security on your Controllers - Itenium\",\"isPartOf\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/controller-security.png\",\"datePublished\":\"2025-04-13T22:00:00+00:00\",\"dateModified\":\"2025-11-28T12:31:41+00:00\",\"author\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022\"},\"breadcrumb\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/#primaryimage\",\"url\":\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/controller-security.png\",\"contentUrl\":\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/controller-security.png\",\"width\":360,\"height\":300,\"caption\":\"A comic book scene where a superhero (wearing a C# emblem) throws [Authorize] shields at rogue controller methods, cartoon villains named 'Anonymous Access' dodge them, bright bold colors, halftone patterns, comic panels with 'BOOM' and 'SECURED!' captions, playful and energetic, comic book art style\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/demo.sidekick.be\/itenium\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"UnitTest: Check Security on your Controllers\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#website\",\"url\":\"https:\/\/demo.sidekick.be\/itenium\/\",\"name\":\"Itenium\",\"description\":\"We build software. We build careers.\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/demo.sidekick.be\/itenium\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022\",\"name\":\"Usdevelop\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g\",\"caption\":\"Usdevelop\"},\"sameAs\":[\"https:\/\/demo.sidekick.be\/itenium\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"UnitTest: Check Security on your Controllers - Itenium","robots":{"index":"noindex","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"og_locale":"en_US","og_type":"article","og_title":"UnitTest: Check Security on your Controllers - Itenium","og_description":"Adding the [AllowAnonymous] or [Authorize(\"policy\")] whenever a new Controller Action Method is added, it\u2019s something that is easily forgotten.\u2026 As I noticed when I was looking at some of our controllers \ud83d\ude35Of course I have written this test many times before. These days it can be quickly re-created with AI but to avoid having to debug its code, here is the copy pasta version, with some possible variations.","og_url":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/","og_site_name":"Itenium","article_published_time":"2025-04-13T22:00:00+00:00","article_modified_time":"2025-11-28T12:31:41+00:00","og_image":[{"width":360,"height":300,"url":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/controller-security.png","type":"image\/png"}],"author":"Usdevelop","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Usdevelop","Est. reading time":"3 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/","url":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/","name":"UnitTest: Check Security on your Controllers - Itenium","isPartOf":{"@id":"https:\/\/demo.sidekick.be\/itenium\/#website"},"primaryImageOfPage":{"@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/#primaryimage"},"image":{"@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/#primaryimage"},"thumbnailUrl":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/controller-security.png","datePublished":"2025-04-13T22:00:00+00:00","dateModified":"2025-11-28T12:31:41+00:00","author":{"@id":"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022"},"breadcrumb":{"@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/#primaryimage","url":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/controller-security.png","contentUrl":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/controller-security.png","width":360,"height":300,"caption":"A comic book scene where a superhero (wearing a C# emblem) throws [Authorize] shields at rogue controller methods, cartoon villains named 'Anonymous Access' dodge them, bright bold colors, halftone patterns, comic panels with 'BOOM' and 'SECURED!' captions, playful and energetic, comic book art style"},{"@type":"BreadcrumbList","@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/dotnet\/check-web-api-security\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/demo.sidekick.be\/itenium\/"},{"@type":"ListItem","position":2,"name":"UnitTest: Check Security on your Controllers"}]},{"@type":"WebSite","@id":"https:\/\/demo.sidekick.be\/itenium\/#website","url":"https:\/\/demo.sidekick.be\/itenium\/","name":"Itenium","description":"We build software. We build careers.","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/demo.sidekick.be\/itenium\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022","name":"Usdevelop","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g","caption":"Usdevelop"},"sameAs":["https:\/\/demo.sidekick.be\/itenium"]}]}},"_links":{"self":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts\/1347","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/comments?post=1347"}],"version-history":[{"count":2,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts\/1347\/revisions"}],"predecessor-version":[{"id":2144,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts\/1347\/revisions\/2144"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/media\/1959"}],"wp:attachment":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/media?parent=1347"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/categories?post=1347"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/tags?post=1347"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}