Home > Mobile >  Configuring multiple alternate authentication options with precedence in WebApi
Configuring multiple alternate authentication options with precedence in WebApi

Time:05-27

I am unclear on how to achieve multiple alternate authentication options with precedence -

I can successfully separately implement

  • OAuth (Single Sign On)
  • API key Authentication (passed as a header)

What I am unclear on - how do I configure it so that if the API key header is present and the API key is valid to process the request; that is if you have a valid API-Key, you do not need the SSO. If no API-Key is presented then you should expect to pass SSO.

The default should be SSO - hence I have the following configured in my Program.cs

builder.Services
        .AddAuthentication(
            options =>
            {
                // If an authentication cookie is present, use it to get authentication information
                options.DefaultAuthenticateScheme =
                    CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                // If authentication is required, and no cookie is present, 
                // use OAuth to sign in
                options.DefaultChallengeScheme = "OAuth";
            }
        )
        .AddCookie(
            options =>
            {
                options.AccessDeniedPath = "/accessdenied";
            }
        )
        .AddOAuth(
            "OAuth",
            (OAuthOptions options) =>
            {
             // abstracted out but takes care of claims, etc...   
             WebApp.Utils.OAuthHelper.Process(options, OAuthConfig);
            }
        );

But my question is - how do I configure this to say - if API Key Header is present, don't bother with OAuth.

CodePudding user response:

The AuthenticationSchemeOptions class has ForwardDefaultSelector that allows any AuthenticationHandler to forward authentication events to another handler.

So what you can do is make your ApiKeyAuthenticationHandler the detault and make it forward authentication events to OAuth handler if API Key header is not present in the HTTP headers.

For example: Let's say you implement ApiKeyAuthenticationHandler as Follows:

// API Key options
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
    public string ApiKeyHeaderName { get; set; } = "X-MY-API-KEY";

    // Add more options here if needed
}

// API Key Authentication handler
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    // Inject any additional services needed by your key handler here
    public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
    {
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // Validation logic here
        // Check if the header exists
        if (!Context.Request.Headers.TryGetValue(Options.ApiKeyHeaderName, out var headerValues))
        {
            // Header is not present fail
            return AuthenticateResult.Fail("Invalid API key.");
        }

        // Header is present validate user
        var user = await GetApiKeyUserAsync(headerValues.First());
        if (user == null)
        {
            return AuthenticateResult.Fail("Invalid API key.");
        }

        // API Key is valid, generate a ClaimIdentity and Principal for the authenticated user
        var identity = new ClaimsIdentity(new Claim[]
        {
            new Claim(ClaimTypes.Name, user)
        }, Scheme.Name);

        var principal = new ClaimsPrincipal(identity);
        return AuthenticateResult.Success(new AuthenticationTicket(principal, null, Scheme.Name));
    }

    // Key validation logic here and you can map an API key to a user
    private async Task<string?> GetApiKeyUserAsync(string key)
    {
        // Add your implementation here
        if (key == "Alice's Key")
        {
            return "Alice";
        }

        if (key == "Bob's Key")
        {
            return "Bob";
        }

        return null;
    }
}

Now in the startup class call AddAuthentication and make "ApiKey" the default, and add a Selector function that checks the HTTP Headers for the presence of a key, if the key is present, then return null and let the Api key handler handle the authentication, otherwiser forward to "OAuth".

builder.Services
    .AddAuthentication(
        options =>
        {
            options.DefaultAuthenticateScheme =
                "ApiKey";
        }
    )
    .AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>("ApiKey", apiKeyOptions =>
    {
        // This scheme will forward Authentication to OAUTH if API Key header is not present
        apiKeyOptions.ForwardDefaultSelector = (context) =>
        {
            // Check the HTTP Request for the presence of Header
            if (!context.Request.Headers.TryGetValue(apiKeyOptions.ApiKeyHeaderName, out var headerValues))
            {
                // Header is not present, forward to OAuth
                return "OAuth"; // "OAuth" is the scheme name you specified when you called "AddOAuth()
            }

            // Do not forward the authentication
            return null;
        };
    })
    // Add other schemes here

  • Related