Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a076f15
feat(auth): add auth policies to operations
Fresa Jan 22, 2026
dc6cf6a
test(auth): extend test apis with auth directives
Fresa Jan 22, 2026
7aaff8a
feat: generate security schemes
Fresa Jan 23, 2026
ec31c19
generate security schemes in root namespace
Fresa Jan 23, 2026
eae28c2
feat(auth): include scope validation
Fresa Jan 23, 2026
17525a7
feat(auth): add security scheme options for configuring scope claim a…
Fresa Jan 24, 2026
4fc34ba
fix(auth): add OpenAPI compatible authorization handler for security …
Fresa Jan 24, 2026
91e1504
refactor(auth): remove security directives when there is no auth spec…
Fresa Jan 24, 2026
2710eea
add auth dependencies
Fresa Jan 24, 2026
2f25a93
test: add OIDC auth test handler
Fresa Jan 25, 2026
40c4ed0
fix indentations
Fresa Jan 25, 2026
3057479
add missing security requirements
Fresa Jan 25, 2026
310fd0b
tests: extract auth extensions
Fresa Jan 25, 2026
3b65dd4
test: add oauth2 implicit flow example
Fresa Jan 25, 2026
d38052a
test: add apikey authentication
Fresa Jan 26, 2026
aaa5efb
refactor: separate request binding to a filter to be able to use requ…
Fresa Jan 27, 2026
2a30d90
add missing nullable directive
Fresa Jan 29, 2026
227eca7
refactor(auth): use a custom endpoint filter for security requirements
Fresa Jan 29, 2026
2f7e860
remove unused auth code
Fresa Jan 29, 2026
c18d0d8
generate Anonymous security filter if operation doesn't define securi…
Fresa Jan 30, 2026
c3fd979
only generate auth response handlers if the operation requires auth
Fresa Jan 30, 2026
6b21765
conditionally render default auth response classes
Fresa Jan 30, 2026
7342dd4
validate auth responses
Fresa Jan 30, 2026
836cbc8
feat(auth): add support for api key parameter resolution
Fresa Feb 3, 2026
6978037
handle operations that have not defined security scheme parameters
Fresa Feb 4, 2026
b585c93
extract finding security scheme parameters to separate method
Fresa Feb 4, 2026
849478a
generate GetParameter for the auth scheme if there is exactly one par…
Fresa Feb 4, 2026
abe37f5
remove duplicated semicolon
Fresa Feb 4, 2026
9b4be1c
test(auth): add api key validation
Fresa Feb 4, 2026
3a5a1ab
doc(auth): add code comments about how to consume api key parameters
Fresa Feb 4, 2026
a31b0ff
adjust code formatting
Fresa Feb 4, 2026
52f580d
doc(auth): add section about configuring authentication and authoriza…
Fresa Feb 4, 2026
de9a33b
format empty lines
Fresa Feb 5, 2026
7ed503b
test: add all variants of security scheme objects
Fresa Feb 5, 2026
a7ced34
fix(auth): authentication should fail if no auth schemes succeeds wit…
Fresa Feb 5, 2026
bda336e
test: let delete operation be anonymous
Fresa Feb 5, 2026
49e06ad
test(auth): assert 403 response when not authorized
Fresa Feb 5, 2026
2f4cfdd
test(auth): include all security schemes and run 401 and 403 scenarios
Fresa Feb 5, 2026
7c661a4
test(auth): verify that the correct apikey parameter methods are gene…
Fresa Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;

namespace OpenAPI.IntegrationTestHelpers.Auth;

public sealed class HotWiredJwtBackchannelHandler : IPostConfigureOptions<JwtBearerOptions>
{
public void PostConfigure(string? name, JwtBearerOptions options)
{
options.BackchannelHttpHandler = new OIDCAuthHttpHandler();
}
}
20 changes: 20 additions & 0 deletions OpenAPI.IntegrationTestHelpers/Auth/HttpClientAuthExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Net.Http.Headers;

namespace OpenAPI.IntegrationTestHelpers.Auth;

public static class HttpClientAuthExtensions
{
public static HttpClient WithOAuth2ImplicitFlowAuthentication(this HttpClient client, params string[] scopes)
{
client.DefaultRequestHeaders.Authorization =
AuthenticationHeaderValue.Parse($"Bearer {OIDCAuthHttpHandler.GetJwt(scopes)}");
return client;
}

public static HttpClient WithValidBasicAuthCredentials(this HttpClient client)
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Basic", Convert.ToBase64String("admin:password"u8.ToArray()));
return client;
}
}
156 changes: 156 additions & 0 deletions OpenAPI.IntegrationTestHelpers/Auth/OIDCAuthHttpHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;

namespace OpenAPI.IntegrationTestHelpers.Auth;

public sealed class OIDCAuthHttpHandler : HttpMessageHandler
{
private static readonly SigningCredentials _privateKey;
private const string Kid = "test";
private static readonly RSAParameters PrivateRsaParameters;
private static string OidcConfigurationContent { get; }
private static string JwksContent { get; }

static OIDCAuthHttpHandler()
{
using var rsa = new RSACryptoServiceProvider
{
PersistKeyInCsp = false
};

PrivateRsaParameters = rsa.ExportParameters(true);
var securityKey = new RsaSecurityKey(PrivateRsaParameters)
{
KeyId = Base64UrlEncoder.Encode(Kid)
};

_privateKey = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSha256Signature);

OidcConfigurationContent = CreateOidcConfigurationContent();
JwksContent = CreateJwksContent();
}

public static string GetJwt(params string[] scopes) => GenerateJwtToken(scopes);
internal const string Issuer = "https://localhost/";

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.RequestUri == null ||
!request.RequestUri.AbsoluteUri.StartsWith(Issuer))
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError));
}

return request.RequestUri.AbsolutePath switch
{
"/.well-known/openid-configuration" => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(OidcConfigurationContent)
}),
"/oauth/jwks" => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(JwksContent)
}),
_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError))
};
}

private static string GenerateJwtToken(params string[] scopes)
{
var securityTokenDescriptor = new SecurityTokenDescriptor
{
Issuer = Issuer,
Audience = Issuer,
Subject = new ClaimsIdentity(),
Expires = DateTime.UtcNow.AddHours(1),
IssuedAt = DateTime.UtcNow,
SigningCredentials = _privateKey,
Claims = new Dictionary<string, object>
{
["scope"] = string.Join(" ", scopes)
}
};

var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var jwt = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor);
var token = jwtSecurityTokenHandler.WriteToken(jwt);
return token;
}

private static string CreateJwksContent() =>
$$"""
{
"keys" : [
{
"kid": "{{Base64UrlEncoder.Encode(Kid)}}",
"e": "{{Base64UrlEncoder.Encode(PrivateRsaParameters.Exponent)}}",
"kty": "RSA",
"alg": "RS256",
"n": "{{Base64UrlEncoder.Encode(PrivateRsaParameters.Modulus)}}"
}
]
}
""";

private static string CreateOidcConfigurationContent() =>
$$"""
{
"issuer":"{{Issuer}}",
"authorization_endpoint":"{{Issuer}}oauth/auz/authorize",
"token_endpoint":"{{Issuer}}oauth/oauth20/token",
"userinfo_endpoint":"{{Issuer}}/oauth/userinfo",
"jwks_uri":"{{Issuer}}oauth/jwks",
"scopes_supported":[
"READ",
"WRITE",
"DELETE",
"openid",
"scope",
"profile",
"email",
"address",
"phone"
],
"response_types_supported":[
"code",
"code id_token",
"code token",
"code id_token token",
"token",
"id_token",
"id_token token"
],
"grant_types_supported":[
"authorization_code",
"implicit",
"client_credentials",
"urn:ietf:params:oauth:grant-type:jwt-bearer"
],
"subject_types_supported":[
"public"
],
"id_token_signing_alg_values_supported":[
"RS256"
],
"id_token_encryption_alg_values_supported":[
"RSA-OAEP",
"RSA-OAEP-256"
],
"id_token_encryption_enc_values_supported":[
"A256GCM"
],
"token_endpoint_auth_methods_supported":[
"client_secret_post",
"client_secret_basic",
"client_secret_jwt",
"private_key_jwt"
],
"token_endpoint_auth_signing_alg_values_supported":[
"RS256"
]
}
""";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace OpenAPI.IntegrationTestHelpers.Auth;

public static class ServiceCollectionAuthExtensions
{
public static IServiceCollection InjectJwtBackChannelHandler(this IServiceCollection services)
{
services.Insert(0,
ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, HotWiredJwtBackchannelHandler>());
return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;

namespace OpenAPI.IntegrationTestHelpers.Observability;

public static class WebHostBuilderExtensions
{
public static IWebHostBuilder AddLogging(this IWebHostBuilder builder) =>
builder.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
logging.AddDebug();
logging.SetMinimumLevel(LogLevel.Trace);

logging.AddFilter("Microsoft.AspNetCore.Authentication", LogLevel.Trace);
logging.AddFilter("Microsoft.AspNetCore.Authorization", LogLevel.Trace);
logging.AddFilter("Microsoft.AspNetCore.Routing", LogLevel.Trace);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.12" />
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions OpenAPI.WebApiGenerator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenApi32", "tests\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenApi32.IntegrationTests", "tests\Example.OpenApi32.IntegrationTests\Example.OpenApi32.IntegrationTests.csproj", "{CFC6595B-DAA2-4866-AE50-6A9AD7863160}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenAPI.IntegrationTestHelpers", "OpenAPI.IntegrationTestHelpers\OpenAPI.IntegrationTestHelpers.csproj", "{58614455-A749-4B08-A8B9-DF876D8262AC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.OpenApi", "tests\Example.OpenApi\Example.OpenApi.csproj", "{4E274740-E49C-4E56-9B69-C33D9409C119}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -158,10 +164,35 @@ Global
{CFC6595B-DAA2-4866-AE50-6A9AD7863160}.Release|x64.Build.0 = Release|Any CPU
{CFC6595B-DAA2-4866-AE50-6A9AD7863160}.Release|x86.ActiveCfg = Release|Any CPU
{CFC6595B-DAA2-4866-AE50-6A9AD7863160}.Release|x86.Build.0 = Release|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Debug|x64.ActiveCfg = Debug|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Debug|x64.Build.0 = Debug|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Debug|x86.ActiveCfg = Debug|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Debug|x86.Build.0 = Debug|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Release|Any CPU.Build.0 = Release|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Release|x64.ActiveCfg = Release|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Release|x64.Build.0 = Release|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Release|x86.ActiveCfg = Release|Any CPU
{58614455-A749-4B08-A8B9-DF876D8262AC}.Release|x86.Build.0 = Release|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Debug|x64.ActiveCfg = Debug|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Debug|x64.Build.0 = Debug|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Debug|x86.ActiveCfg = Debug|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Debug|x86.Build.0 = Debug|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|Any CPU.Build.0 = Release|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|x64.ActiveCfg = Release|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|x64.Build.0 = Release|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|x86.ActiveCfg = Release|Any CPU
{4E274740-E49C-4E56-9B69-C33D9409C119}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{4E274740-E49C-4E56-9B69-C33D9409C119} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ These handlers will not be generated in subsequent compilations as the generator
<RemoveDir Directories="$(CompilerGeneratedFilesOutputPath)" />
</Target>
```

## Authentication and Authorization
OpenAPI defines [security scheme objects](https://spec.openapis.org/oas/latest#security-scheme-object) for authentication and authorization mechanisms. The generator implement endpoint filters that corresponds to the security declaration of each operation. Do _not_ call `UseAuthentication` or similar when configuring the application.

The security schemes for the [security requirements](https://spec.openapis.org/oas/latest#security-requirement-object) declared by the operations must be implemented. Use the familiar `AddAuthentication` builder method to register each scheme. Security scheme object configurations are generated to the `SecuritySchemes` class and can be used to configure the scheme implementations.

## Dependency Injection
Operations are registered as scoped dependencies. Any dependencies can be injected into them as usual via the app builder's `IServiceCollection`.

Expand Down
25 changes: 18 additions & 7 deletions src/OpenAPI.WebApiGenerator/ApiGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Microsoft.OpenApi;
using OpenAPI.WebApiGenerator.CodeGeneration;
using OpenAPI.WebApiGenerator.Extensions;
using OpenAPI.WebApiGenerator.OpenApi;
Expand Down Expand Up @@ -66,7 +68,12 @@ private static void GenerateCode(SourceProductionContext context,
var jsonValidationExceptionGenerator = new JsonValidationExceptionGenerator(rootNamespace);
jsonValidationExceptionGenerator.GenerateJsonValidationExceptionClass().AddTo(context);

var endpointGenerator = new OperationGenerator(compilation, jsonValidationExceptionGenerator, options);
var authGenerator = new AuthGenerator(openApi);
var endpointGenerator = new OperationGenerator(
compilation,
jsonValidationExceptionGenerator,
authGenerator,
options);

var httpRequestExtensionsGenerator = new HttpRequestExtensionsGenerator(
openApiVersion,
Expand All @@ -77,13 +84,14 @@ private static void GenerateCode(SourceProductionContext context,
openApiVersion);
httpResponseExtensionsGenerator.GenerateHttpResponseExtensionsClass().AddTo(context);

var apiConfigurationGenerator = new ApiConfigurationGenerator(rootNamespace);
var apiConfigurationGenerator = new ApiConfigurationGenerator(rootNamespace, authGenerator);
apiConfigurationGenerator.GenerateClass().AddTo(context);

var validationExtensionsGenerator = new ValidationExtensionsGenerator(rootNamespace);
validationExtensionsGenerator.GenerateClass().AddTo(context);

var operations = new List<(string Namespace, HttpMethod HttpMethod)>();
var operations = new List<(string Namespace, KeyValuePair<HttpMethod, OpenApiOperation> Operation)>();
var securityParameterGenerators = new ConcurrentDictionary<IOpenApiSecurityScheme, List<ParameterGenerator>>();
foreach (var path in openApi.Paths)
{
var pathExpression = path.Key;
Expand All @@ -106,7 +114,6 @@ private static void GenerateCode(SourceProductionContext context,
var operationMetadata = TypeMetadata.From(openApiOperationVisitor.Pointer);
var operationDirectory = operationMetadata.Path;
var operationNamespace = $"{rootNamespace}.{operationMetadata.Namespace}.{operationMetadata.Name}";
var operationMethod = openApiOperation.Key;
var operation = openApiOperation.Value;
var operationParameterGenerators = new Dictionary<string, ParameterGenerator>(pathParameterGenerators);

Expand Down Expand Up @@ -193,12 +200,13 @@ private static void GenerateCode(SourceProductionContext context,
operationDirectory);
responseSourceCode.AddTo(context);

operations.Add((operationNamespace, operationMethod));
operations.Add((operationNamespace, openApiOperation));
var endpointSource = endpointGenerator
.Generate(operationNamespace,
operationDirectory,
pathExpression,
operationMethod);
(openApiOperation.Key, openApiOperation.Value),
operationParameterGenerators.Values.ToArray());
endpointSource
.AddTo(context);
}
Expand All @@ -213,7 +221,10 @@ private static void GenerateCode(SourceProductionContext context,
}
}

var operationRouterGenerator = new OperationRouterGenerator(rootNamespace);
authGenerator.GenerateSecuritySchemeClass(rootNamespace)?.AddTo(context);
authGenerator.GenerateSecuritySchemeOptionsClass(rootNamespace)?.AddTo(context);
authGenerator.GenerateSecurityRequirementsFilter(rootNamespace)?.AddTo(context);
var operationRouterGenerator = new OperationRouterGenerator(rootNamespace, authGenerator);
operationRouterGenerator.ForMinimalApi(operations).AddTo(context);
}

Expand Down
Loading
Loading