-
-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding security schemes to the api interface generator #106
Comments
@Roflincopter first of all, thank you for bringing this up and for your interest in this To ensure I understand, are you looking into generating code based on security schemes defined in the OpenAPI specification file? I have only used Refit with bearer token authorization and for this approach, I keep security concerns out of the API itself. What I like about Refit is that they allow the developer to easily customize the instance of the HttpClient used by the generated code. I mostly work on the backend of systems and what I normally do is implement HttpClient message handlers for acquiring an access token, as a confidential client using a client/secret persisted securely in some arbitrary secret store (e.g. Azure Keyvault), or using the managed identity service and the DefaultAzureCredentials class from Azure.Identity Refit provides convenient extension methods to static IServiceCollection ConfigureApiClient(this IServiceCollection services)
{
services.AddTransient<ApiAuthenticationHandler>();
services
.AddRefitClient<IApiClient>()
.ConfigureHttpClient(c => c.BaseAddress = GetApiBaseAddress())
.AddHttpMessageHandler<ApiAuthenticationHandler>();
return services;
} where the authentication handler might look something like this: class ApiAuthenticationHandler : DelegatingHandler
{
private readonly TokenRequestContext context;
public ApiAuthenticationHandler()
{
context = new TokenRequestContext(new[] { "https://api.foo.com/dev/bar/.default" });
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Headers.Authorization = await GetTokenAsync(cancellationToken);
var response = await base.SendAsync(request, cancellationToken);
return response;
}
private async Task<AuthenticationHeaderValue?> GetTokenAsync(CancellationToken cancellationToken)
{
var credentials = new DefaultAzureCredential(AzureCredentialOptions.CreateDefault());
var token = await credentials.GetTokenAsync(context, cancellationToken);
return new AuthenticationHeaderValue("Bearer", token.Token);
}
} In most, if not all cases, I would also configure things like HTTP telemetry logging and retry policies with Polly at the HttpClient level static IServiceCollection ConfigureApiClient(this IServiceCollection services)
{
services.AddTransient<ApiAuthenticationHandler>();
services
.AddRefitClient<IApiClient>()
.ConfigureHttpClient(c => c.BaseAddress = GetApiBaseAddress())
.AddHttpMessageHandler<ApiAuthenticationHandler>()
.AddHttpMessageHandler<TelemetryDelegatingHandler>()
.AddPolicyHandler(
HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(
Backoff.DecorrelatedJitterBackoffV2(
TimeSpan.FromSeconds(1),
6)))
return services;
} Where the telemetry handler might look something like this: class TelemetryDelegatingHandler : DelegatingHandler
{
private readonly ITelemetryClient telemetryClient;
public TelemetryDelegatingHandler(ITelemetryClient telemetryClient)
{
this.telemetryClient = telemetryClient;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
return await WriteTelemetry(request, response);
}
private async Task<HttpResponseMessage> WriteTelemetry(
HttpRequestMessage request,
HttpResponseMessage response)
{
try
{
var telemetry = new TraceTelemetry(
"Outbound HTTP Request",
!response.IsSuccessStatusCode ? SeverityLevel.Error : SeverityLevel.Verbose);
// Extract request and response details to trace telemetry properties
telemetryClient.TrackTrace(telemetry);
}
catch (Exception e)
{
telemetryClient.TrackException(e);
}
return response;
}
} Sorry for the excessive explanations and examples. You probably already know all that, but since I can't really know I wrote it anyway So back to the point, are you interested or looking into generating code that redefines headers in the refit interface? Like this: [Headers("Authorization: Basic YmFzaWMtYXV0aG9yaXphdGlvbi1pczpnZW5lcmFsbHktYS1iYWQtaWRlYQ==")]
public interface ISomeApi
{
[Get("/things/{id}")]
Task GetSomething(string id);
} or are you perhaps after generating the boilerplate code for configuring the HttpClient that Refit uses, like in the first example? As for implementation details, your actual inquiry (again, sorry that I digress), I recently introduced the IRefitGeneratorInterface which is designed to have multiple implementations based on the RefitGeneratorSettings and how the code should be generated If we are to support generating code based on security schemes then implementing It might also be interesting to generate boilerplate code and convenience extension methods to IServiceCollection, but that will also need to be configured from settings since not everyone using Refitter is building server systems Thanks for bringing this up again @Roflincopter |
@all-contributors please add @Roflincopter for ideas |
I've put up a pull request to add @Roflincopter! 🎉 |
I just saw the Authorization attribute in the refit documentation and thought i would be nice to add it to the Interface generator. But as soon as I started thinking about how to implement it there was no clear approach how to because then you would either have to configure the refit http client with right options, which might not be very clear that you have to do. Or you generate methods with explicit parameters for each security scheme you support which leads to an increase of methods on the interface.
Implementing it with HttpMessageHandlers seems like an alright choice, I could still take a look at adding the "Authorization" attributes to the methods, so you have to option to configure it in the client without writing an HttpMessageHandler for it.
But then I would have to detect and emit the right attributes, and I have to make sure I don't break the existing method of writing your own HttpMessageHandler. And I don't have a way to test OAuth authentication methods. |
Have you started on this @Roflincopter ? I was thinking that it would be cool to have a static IServiceCollection ConfigureApiClient(this IServiceCollection services)
{
services.AddTransient<AzureAuthenticationHandler >(); // Only generate this if '--azure-client' was specified
services
.AddRefitClient<IApiClient>()
.ConfigureHttpClient(c => c.BaseAddress = "[base url value]")
.AddHttpMessageHandler<AzureAuthenticationHandler >() // Only generate this if '--azure-client' was specified
return services;
} The message handler should only be generated if class AzureAuthenticationHandler : DelegatingHandler
{
private readonly TokenRequestContext context;
public ApiAuthenticationHandler()
{
context = new TokenRequestContext(new[] { "[OAuth scope defined from CLI argument]" });
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Headers.Authorization = await GetTokenAsync(cancellationToken);
var response = await base.SendAsync(request, cancellationToken);
return response;
}
private async Task<AuthenticationHeaderValue?> GetTokenAsync(CancellationToken cancellationToken)
{
var credentials = new DefaultAzureCredential(new DefaultAzureCredentialOptions());
var token = await credentials.GetTokenAsync(context, cancellationToken);
return new AuthenticationHeaderValue("Bearer", token.Token);
}
} If you haven't already started on something like this, then I can do it myself. But if you already have something similar in the works then let's stick with yours |
Not started yet, still thinking on what I want and how sane it is, also less free time to work on this as I had hoped. I was rather thinking of using the refitter Generally you will only use one type of authentication in your project anyways and generally all the authorized calls will support all the of the possible authentication methods. That is an assumption on my side but I think it's a sane assumption. Then for each method that supports the given security-scheme, I will generate a method with the method level attribute: Each method that has no security schemes will not have the attribute, Each method that has other security schemes but not the one you chose will not be generated? (Will this ever happen?) Then you can configure the refit library to register a callback for authorization header input. So you can do this without attaching a generic header manipulator/adder class to the http client. But rather some lightweight lambda. to clariy my last paragraph; an except from the refit github page.
|
Take your time @Roflincopter. Time is precious and it is the only non-renewable resource we humans have. I just think that its awesome that there is life in this little thing I started
Do you mind if we use
It's normal for clients to use a single authentication method, but its also normal for servers to offer multiple authentication methods, all depending on what sorts of clients are communicating with it. OAuth bearer tokens are nice since you can have claims, roles, scopes, and complex sets of customized claims, to describe what resources the client can access based on the clients identity. OAuth bearer tokens on the other hand requires a secure token service that owns the claims used by the server and grants access to the client. Client Certificates are secure but there is no way elegant way to define to which resources the client can access, at least nothing as elegant as OAuth scopes. Basic Authentication rarely makes sense, except for web hooks where the client has no real ties to the server but was configured to call endpoints on it, for basic authentication it will also make sense to have security on the message level, like HMAC signatures, and have the server to just ignore unknown/invalid/unsigned messages
I like this 👍
I can't see a scenario where the same client uses multiple security schemes, but it is common practice to have multiple security schemes to cater to different types of clients. I've built quite a few API's that both have OAuth 2.0 Bearer Tokens and Client Certificates that are used separately by different client systems
I had no idea that this was possible! |
Just started using refitter recently. Thanks for this software! That said, is there any love or ETA towards this (no commentary for ~8 months)? Every time I regenerate my bindings, I must manually add the attribute to my secure endpoints (only a subset requires authorization) and I worry that I, or a teammate will miss endpoints. [Headers("Authorization: Bearer")] |
@jbt00000 The idea died out a bit and I think its because most users figured out that they can configure authorization through HttpClient delegating handlers that you can configure Refit to use using the Refit.HttpClientFactory tooling I usually have something like this configured: static IServiceCollection ConfigureApiClient(this IServiceCollection services)
{
services.AddTransient<AzureAuthenticationHandler >(); // Only generate this if '--azure-client' was specified
services
.AddRefitClient<IApiClient>()
.ConfigureHttpClient(c => c.BaseAddress = "[base url value]")
.AddHttpMessageHandler<AzureAuthenticationHandler >() // Only generate this if '--azure-client' was specified
return services;
} If you're using .NET Core then Refitter can generate {
"openApiPath": "./OpenAPI/v3.0/petstore.json",
"namespace": "Petstore",
"outputFolder": "GeneratedCode",
"outputFilename": "SwaggerPetstoreDirect.cs",
"naming": {
"useOpenApiTitle": false,
"interfaceName": "SwaggerPetstoreDirect"
},
"dependencyInjectionSettings": {
"baseUrl": "https://petstore3.swagger.io/api/v3",
"usePolly": true,
"pollyMaxRetryCount": 3,
"firstBackoffRetryInSeconds": 0.5,
"httpMessageHandlers": [
"AzureAuthenticationHandler"
]
},
"codeGeneratorSettings": {
"dateType": "System.DateTime",
"dateTimeType": "System.DateTime",
"arrayType": "System.Collections.Generic.IList"
},
"operationNameGenerator": "default",
"typeAccessibility": "internal"
} and using Refitter with the refitter --settings-file petstore.refitter The generated code will contain this extension method: namespace Petstore
{
using System;
using Microsoft.Extensions.DependencyInjection;
using Polly;
using Polly.Contrib.WaitAndRetry;
using Polly.Extensions.Http;
public static partial class IServiceCollectionExtensions
{
public static IServiceCollection ConfigureRefitClients(this IServiceCollection services, Action<IHttpClientBuilder>? builder = default)
{
var clientBuilderISwaggerPetstoreDirect = services
.AddRefitClient<ISwaggerPetstoreDirect>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://petstore3.swagger.io/api/v3"))
.AddHttpMessageHandler<AzureAuthenticationHandler>()
.AddPolicyHandler(
HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(
Backoff.DecorrelatedJitterBackoffV2(
TimeSpan.FromSeconds(0.5),
3)));
builder?.Invoke(clientBuilderISwaggerPetstoreDirect);
return services;
}
}
} hope this helps @jbt00000 |
How about this solution? internal static class ParameterExtractor
{
public static IEnumerable<string> GetParameters(
CSharpOperationModel operationModel,
OpenApiOperation operation,
RefitGeneratorSettings settings,
string dynamicQuerystringParameterType,
out string? dynamicQuerystringParameters)
{
var routeParameters = operationModel.Parameters
.Where(p => p.Kind == OpenApiParameterKind.Path)
.Select(p => $"{JoinAttributes(GetAliasAsAttribute(p))}{p.Type} {p.VariableName}")
.ToList();
var queryParameters =
GetQueryParameters(operationModel, settings, dynamicQuerystringParameterType, out dynamicQuerystringParameters);
var bodyParameters = operationModel.Parameters
.Where(p => p.Kind == OpenApiParameterKind.Body && !p.IsBinaryBodyParameter)
.Select(p =>
$"{JoinAttributes("Body", GetAliasAsAttribute(p))}{GetParameterType(p, settings)} {p.VariableName}")
.ToList();
var headerParameters = new List<string>();
if (settings.GenerateOperationHeaders)
{
headerParameters = operationModel.Parameters
.Where(p => p.Kind == OpenApiParameterKind.Header && p.IsHeader)
.Select(p =>
$"{JoinAttributes($"Header(\"{p.Name}\")")}{GetParameterType(p, settings)} {p.VariableName}")
.ToList();
}
// Add
if (settings.GenerateAuthenticationHeader)
{
var document = operation.Parent.Parent;
foreach (var securitySchemeName in operationModel.Security.SelectMany(x => x.Keys))
{
if (!document.SecurityDefinitions.TryGetValue(securitySchemeName, out var securityScheme))
{
continue;
}
if (securityScheme.Type == OpenApiSecuritySchemeType.Http || securityScheme.Type == OpenApiSecuritySchemeType.OAuth2)
{
headerParameters.Add($"[Authorize(\"{securityScheme.Scheme ?? "Bearer"}\")] string bearerToken");
break;
}
if (securityScheme.Type == OpenApiSecuritySchemeType.ApiKey
&& securityScheme.In == OpenApiSecurityApiKeyLocation.Header
&& !operationModel.Parameters.Any(p => p.Kind == OpenApiParameterKind.Header && p.IsHeader && p.Name == securityScheme.Name))
{
headerParameters.Add($"[Header(\"{securityScheme.Name}\")] string apiKey");
break;
}
}
}
... |
Sorry for the late response @kmfd3s, I went offline for the Christmas holidays The code snippet looks fine to me. I'm fine with getting it in if the change doesn't break existing features. Feel free to create a pull request and I'll make sure that we get it merged in and released |
I'm currently looking at adding security schemes to the Api generator, but am unsure how to handle it. I suspect that it have to be separate functions, or maybe seperate interfaces, for each securityscheme defined one? But I'm unsure what would be the better way forward and wanted to discuss it.
The text was updated successfully, but these errors were encountered: