Refit - Adding Bearer Authentication

December 19, 2023#Software Development
Series: Refit
Article
Author image.

Scott DePouw, Senior Consultant

Refit exposes several settings and options. One such option enables the ability to add bearer tokens (any headers, really) to outgoing requests. To get this working for the subject in question, online resources always skipped a crucial step: Obtaining the bearer token. Spoiler alert for later: We’ll eventually define a lambda operation that returns the token. Online content providing examples on how to add bearer tokens to Refit requests always stubbed out this part, just having GetTokenAsync() that was never defined. I’m here to shed light on that facet, along with the whole process start to finish, implementing it in a DI-friendly manner. Let’s go!

Full Example Source Code

A complete example using the code in this post is available here.

Cat Facts: Serious Business

In this post we will be pretending our beloved Cat Facts endpoint now requires a bearer token. The Cat Facts API exposes a new endpoint, /oauth, that takes a client ID/secret and returns a token string. We now have to adjust our existing Refit client so that all requests to get cat facts now have an OAuth bearer token in the header.

A Quick and Dirty Approach

One way to accomplish this would be to manually call to the /oauth endpoint and then pass this token around our code anywhere it’s needed. This isn’t great, as we would have to be getting the received token anywhere we want to use our Refit client, and pass it in via method parameter to our client whenever we need it. Thankfully Refit gives us a better solution, where it handles the OAuth token fetching and attaching to headers for us.

Modifying ICatFactsClient

We’ll start by changing our Refit client definition, exposing the new /oauth endpoint and decorating the facts endpoint to indicate it requires OAuth:

public class OAuthRequest
{
  public string ClientId { get; set; } = "";
  public string ClientSecret { get; set; } = "";
}

public interface ICatFactsClient
{
  [Post("/oauth")]
  public Task<ApiResponse<string?>> GetBearerTokenAsync(OAuthRequest request, CancellationToken cancellationToken);
    
  [Get("/facts")]
  [Headers("Authorization: Bearer")] // Adding the header to each request for facts
  Task<ApiResponse<List<CatFact>>> GetTheFactsAsync(CancellationToken cancellationToken);
}

We’ve defined a simple DTO in the form of OAuthRequest, that will now be POSTed to the /oauth endpoint, via GetBearerTokenAsync. GetTheFactsAsync is now decorated with a HeadersAttribute (a Refit attribute), dictating a Bearer header be added to every request.

Let’s also define a DTO that’ll house settings for our RefitSandbox application. We’ll omit wiring this up to appsettings.json and assume we can get IOptions<RefitSandboxSettings> wherever we want.

public class RefitSandboxSettings
{
  public string ClientId { get; set; } = "";
  public string ClientSecret { get; set; } = "";
  // Other settings not related to OAuth would also be here. 
}

With our Refit client modified, now we need to instruct Refit how to hydrate the header we’ve said will be on every facts request going forward.

Refit’s AuthorizationHeaderValueGetter Setting

Until now we’ve been relying on the default RefitSettings, but now we have something to set. The AddRefitClient<TClient>() method takes in a RefitSettings instance as a parameter, and one of the available settings is AuthorizationHeaderValueGetter, a Func<HttpRequestMessage,CancellationToken,Task<string>>. This is where existing online examples always fell flat for me:

RefitSettings settings = new()
{
  AuthorizationHeaderValueGetter = (message, cancellationToken) => GetTokenAsync() // Lame! What does this do?
}

How does GetTokenAsync() work? Who knows! It returns Task<string>, but how do you access the client to call that endpoint? From what I can tell, this stub always assumes you just have the bearer token string on hand already. Not very useful in practical scenarios, which typically require you to actively fetch the token (either from cache or from the source if you don’t have one yet). This is the point where the quick-and-dirty route would suffice: You could new up an instance of HttpClient in GetTokenAsync(), set it up with all the Cat Facts settings again, and call the endpoint. That’s rather redundant, considering we’ve already set up a Refit client with all that info.

Let’s instead take advantage of Refit and make it call itself. To do so, we’d ideally want to get an instance of ICatFactsClient via dependency injection. We can’t just say new CatFactsClient() (remember the implementation is auto-generated code) after all. How do we do that in the middle of Program.cs before we’ve even built our service provider or host? The Func we get from Refit doesn’t give us access to either of those. What do we do?

Introducing AuthBearerTokenFactory

We need to be able to get our client properly. Let’s make a class that defines a means to get the bearer token string, with the assumption that it’ll have access to a service provider at the time of calling. It’ll be a static class, as we’ll need it during Program.cs initialization.

public static class AuthBearerTokenFactory
{
  private static Func<CancellationToken, Task<string>>? _getBearerTokenAsyncFunc;
  
  /// <summary>
  /// Provide a delegate that returns a bearer token to use for authorization
  /// </summary>
  public static void SetBearerTokenGetterFunc(Func<CancellationToken, Task<string>> getBearerTokenAsyncFunc)
    => _getBearerTokenAsyncFunc = getBearerTokenAsyncFunc;

  public static Task<string> GetBearerTokenAsync(CancellationToken cancellationToken)
  {
    if (_getBearerTokenAsyncFunc is null) throw new InvalidOperationException("Must set Bearer Token Func before using it!");
    return _getBearerTokenAsyncFunc!(cancellationToken);
  }
}

We define two methods here, essentially a getter and setter for our function that will get our bearer token. SetBearerTokenGetterFunc will take in a delegate that will get invoked whenever we need an OAuth token. GetBearerTokenAsync will return said delegate (and throw if we haven’t set it yet).

Plugging Our Factory Into Refit

Let’s plug this class into Program.cs and Refit.

// Program.cs
using Microsoft.Extensions.Options;
using Refit;
using RefitSandbox;

// ... other setup code ...

RefitSettings refitSettings = new()
{
  // Tell Refit to use AuthBearerTokenFactory.GetBearerTokenAsync() whenever it needs an OAuth token string.
  // This is a lambda, so it won't be called until a request is made.
  AuthorizationHeaderValueGetter = (_, cancellationToken) => AuthBearerTokenFactory.GetBearerTokenAsync(cancellationToken)
};

builder.Services.AddRefitClient<ICatFactsClient>(refitSettings) // Now passing refitSettings to Refit
  .ConfigureHttpClient(httpClient => /* Same config as before */);

IHost host = builder.Build();
// Define what gets called when Refit requires an OAuth token string
AuthBearerTokenFactory.SetBearerTokenGetterFunc(cancellationToken =>
{
  // Get our application settings and an instance of ICatFactsClient via the host's ServiceProvider.
  RefitSandboxSettings settings = host.Services.GetRequiredService<IOptions<RefitSandboxSettings>>().Value;
  ICatFactsClient client = host.Services.GetRequiredService<ICatFactsClient>();
  return GetTheToken(client, settings, cancellationToken);
});
host.Run();
return;

// Could wrap this in its own dependency, e.g. ICatFactsClientService,
// but for brevity we'll just define a local function here in Program.cs
async Task<string> GetTheToken(ICatFactsClient client, RefitSandboxSettings settings, CancellationToken cancellationToken)
{
  // In reality we'd log this error, but for our demo we'll return a fake token, since
  // our OAuth endpoint is always going to 404.
  const string defaultToken = "default-token-value";
  try
  {
    OAuthRequest request = new() { ClientId = settings.ClientId, ClientSecret = settings.ClientSecret };
    ApiResponse<string?> result = await client.GetBearerTokenAsync(request, cancellationToken);
    // Taking advantage of ApiResponse<T> by checking the status before returning
    await result.EnsureSuccessStatusCodeAsync();
    // Any caching or other logic regarding the full token would be implemented here.
    return result.Content ?? defaultToken; 
  }
  catch (Exception)
  {
    return defaultToken;
  }
}

First thing we do is configure RefitSettings.AuthorizationHeaderValueGetter, hooking it up to AuthBearerTokenFactory.GetBearerTokenAsync(). Now whenever Refit needs a token, it’ll call our factory method.

Then, after we’ve built our host, we now have the ability to get a service via dependency injection. This is how we obtain our ICatFactsClient instance! With this ability, we can now call AuthBearerTokenFactory.SetBearerTokenGetterFunc() and use our client to get our token:

AuthBearerTokenFactory.SetBearerTokenGetterFunc(cancellationToken =>
{
  RefitSandboxSettings settings = host.Services.GetRequiredService<IOptions<RefitSandboxSettings>>().Value;
  ICatFactsClient client = host.Services.GetRequiredService<ICatFactsClient>();
  return GetTheToken(client, settings, cancellationToken);
});

GetTheToken() is just a local function we define at the end of Program.cs. As the comment suggests, this could be abstracted into a service, that wraps a call to ICatFactsClient, to handle caching, error checking, or any other logic. This would also simplify Program.cs, as all we’d have to get from the service provider is an instance of our abstraction, and any dependencies it had would automatically get resolved.

Inside our local function GetTheToken() we’re returning a fake value if the OAuth call fails (which it will in our case, because Cat Facts API has no /oauth endpoint), but we’re still writing this as if it could work.

async Task<string> GetTheToken(ICatFactsClient client, RefitSandboxSettings settings, CancellationToken cancellationToken)
{
  const string defaultToken = "default-token-value";
  try
  {
    OAuthRequest request = new() { ClientId = settings.CientId, ClientSecret = settings.ClientSecret };
    ApiResponse<string?> result = await client.GetBearerTokenAsync(request, cancellationToken);
    await result.EnsureSuccessStatusCodeAsync();
    return result.Content ?? defaultToken; 
  }
  catch (Exception)
  {
    return defaultToken;
  }
}

Watch It Fly

Taking advantage of a certain request/response logging handler we wrote in a previous blog post, we can see exactly what’s happening when we run our application to get some now-protected cat facts by calling await _catFactsClient.GetTheFactsAsync(cancellationToken):

info: RefitSandbox.HttpLoggingHandler[0]
      [12cb854f-7be9-4335-8607-a89182fbf442] Request: Method: POST,
      RequestUri: 'https://cat-fact.herokuapp.com/oauth', Version: 1.1,
      Content: System.Net.Http.PushStreamContent, Headers:
      {
        Transfer-Encoding: chunked
        Content-Type: application/json; charset=utf-8
      }
info: RefitSandbox.HttpLoggingHandler[0]
      [12cb854f-7be9-4335-8607-a89182fbf442] Response: StatusCode: 404,
      ReasonPhrase: 'Not Found', Version: 1.1,
      Content: System.Net.Http.HttpConnectionResponseContent, Headers:
      {
        // [... response headers ...]
      }

// [... other log messages ...]

info: RefitSandbox.HttpLoggingHandler[0]
      [f1f1291f-f44d-440a-901d-84371e235e79] Request: Method: GET,
      RequestUri: 'https://cat-fact.herokuapp.com/facts', Version: 1.1,
      Content: <null>, Headers:
      {
        // [Our Authorization Bearer header and token got added!]
        Authorization: Bearer default-token-value
      }
info: RefitSandbox.HttpLoggingHandler[0]
      [f1f1291f-f44d-440a-901d-84371e235e79] Response: StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.HttpConnectionResponseContent, Headers:
      {
        // [... response headers ...]
      }

We can see that Refit correctly made two HTTP calls when we asked for the one: Refit invoked the delegate we set up to call itself and hit the /oauth endpoint. Of course, this fake endpoint doesn’t exist in the Cat Facts API, so it 404s. So, in the call we actually requested, /facts, it has Authorization: Bearer default-token-value using our default token. If that was a real token, and Cat Facts were actually requiring this token, it would have been allowed!

Jumping Through Hoops for Dependency Injection

Admittedly we accomplished our goal in a roundabout way: Defining a factory that houses a delegate that’s properly fed into Refit, then initializing after we have a ServiceProvider to get what we need to make the call. Even though it makes setting up our application in Program.cs a little more complicated, we now no longer have that complexity anywhere within our actual application code or business logic. We didn’t have to change the way we used ICatFactsClient at all, and continue to just call GetTheFactsAsync(). Refit encourages this setup as well, as it provides the AuthorizationHeaderValueGetter in the first place. I do wish that Refit’s delegate provided us with an instance of the service provider in the first place, as doing so would mean we could access our dependencies much easier!

Encapsulate the Madness?

You can use this “get it working” sort of solution above, and encapsulate or abstract away the complexities so that you don’t have to think when putting together Program.cs. I do this myself (it was beyond the scope of this already-long post to detail it here) since I find myself using Refit in many applications. Perhaps I’ll share some of what I do in a 2024 blog post, wrapping up the application builders .NET has and streamlining a lot of common operations.

Have an enjoyable holiday and a happy new year!


Copyright © 2024 NimblePros - All Rights Reserved