Git Product home page Git Product logo

Comments (9)

Roflincopter avatar Roflincopter commented on July 2, 2024 1

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 [Headers("Authorization: x")] mechanism. The way I was thinking of implementing it was as a commandline option for refitter: --usesecurityscheme "jwt-auth" where "jwt-auth" is one of the security schemes available in the openapi specification you are generating for.

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: [Headers("Authorization: x")] where x is the right type for that security scheme, either bearer or basic, etc.

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.

Bearer Authentication
Most APIs need some sort of Authentication. The most common is OAuth Bearer authentication. A header is added to each request of the form: Authorization: Bearer . Refit makes it easy to insert your logic to get the token however your app needs, so you don't have to pass a token into each method.

  1. Add [Headers("Authorization: Bearer")] to the interface or methods which need the token.
  2. Set AuthorizationHeaderValueGetter in the RefitSettings instance. Refit will call your delegate each time it needs to obtain the token, so it's a good idea for your mechanism to cache the token value for some period within the token lifetime.

from refitter.

jbt00000 avatar jbt00000 commented on July 2, 2024 1

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")]

from refitter.

christianhelle avatar christianhelle commented on July 2, 2024

@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 IServiceCollection in the Refit.HttpClientFactory library. Configuring the Refit client becomes as simple as:

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 IRefitInterfaceGenerator would be the best place to do so. It's of course very important that we respect all the settings configured in the RefitGeneratingSettings instance so that we are not breaking any existing functionality

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

from refitter.

christianhelle avatar christianhelle commented on July 2, 2024

@all-contributors please add @Roflincopter for ideas

from refitter.

allcontributors avatar allcontributors commented on July 2, 2024

@christianhelle

I've put up a pull request to add @Roflincopter! 🎉

from refitter.

Roflincopter avatar Roflincopter commented on July 2, 2024

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.

Task PostSomething([Body] object body, [Authorize("Bearer")] string token, CancellationToken cancellationToken = default);

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.

[Headers("Authorization: Bearer")]

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.

from refitter.

christianhelle avatar christianhelle commented on July 2, 2024

Have you started on this @Roflincopter ?

I was thinking that it would be cool to have a --use-http-client-factory "[base url]" --azure-client "[scope]" CLI tool argument that generates boilerplate code for setting up the Refit client for a App or API running on Azure, like the examples I posted previously

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 --azure-client was specified

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

from refitter.

christianhelle avatar christianhelle commented on July 2, 2024

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.

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

I was rather thinking of using the refitter [Headers("Authorization: x")] mechanism. The way I was thinking of implementing it was as a commandline option for refitter: --usesecurityscheme "jwt-auth" where "jwt-auth" is one of the security schemes available in the openapi specification you are generating for.

Do you mind if we use - as a word separator in CLI arguments? I just think its more readable to see --use-security-scheme than --usesecurityscheme. If you have a better reason then I will welcome it. I have strong opinions but they are weakly held

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.

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

For each method that supports the given security-scheme, I will generate a method with the method level attribute: [Headers("Authorization: x")] where x is the right type for that security scheme, either bearer or basic, etc.

Each method that has no security schemes will not have the attribute,

I like this 👍

Each method that has other security schemes but not the one you chose will not be generated? (Will this ever happen?)

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

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.

I had no idea that this was possible!

from refitter.

christianhelle avatar christianhelle commented on July 2, 2024

@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 IServiceCollection extension methods by using a .refitter settings file. So by having a settings file like this:

{
  "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 --settings-file argument like this:

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

from refitter.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.