Adding a custom AuthenticationHandler to a list of external login providers

1.4k Views Asked by At

I'm using ASP.NET Core 3.1, and I have multiple external login providers configured:

services.AddAuthentication()
    .AddDeviantArt(d => {
        d.Scope.Add("feed");
        d.ClientId = Configuration["Authentication:DeviantArt:ClientId"];
        d.ClientSecret = Configuration["Authentication:DeviantArt:ClientSecret"];
        d.SaveTokens = true;
    })
    .AddTwitter(t => {
        t.ConsumerKey = Configuration["Authentication:Twitter:ConsumerKey"];
        t.ConsumerSecret = Configuration["Authentication:Twitter:ConsumerSecret"];
        t.SaveTokens = true;
    });

I'd like to create a custom AuthenticationProvider that, instead of redirecting to another website, asks for that website's "API key" and treats that as the access token. (The website in question does not support any version of OAuth.)

Before actually hooking it up, I want to test that I can get a custom AuthenticationProvider working at all, so I found one that implements HTTP Basic authentication:

public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> {
    public CustomAuthenticationHandler(
        IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock)
        : base(options, logger, encoder, clock) { }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
        if (!Request.Headers.ContainsKey("Authorization")) {
            return AuthenticateResult.NoResult();
        }

        if (!AuthenticationHeaderValue.TryParse(Request.Headers["Authorization"], out AuthenticationHeaderValue headerValue)) {
            return AuthenticateResult.NoResult();
        }

        if (!"Basic".Equals(headerValue.Scheme, StringComparison.OrdinalIgnoreCase)) {
            return AuthenticateResult.NoResult();
        }

        byte[] headerValueBytes = Convert.FromBase64String(headerValue.Parameter);
        string userAndPassword = Encoding.UTF8.GetString(headerValueBytes);
        string[] parts = userAndPassword.Split(':');
        if (parts.Length != 2) {
            return AuthenticateResult.Fail("Invalid Basic authentication header");
        }
        string user = parts[0];
        string password = parts[1];

        bool isValidUser = true;

        if (!isValidUser) {
            return AuthenticateResult.Fail("Invalid username or password");
        }
        var claims = new[] { new Claim(ClaimTypes.Name, user) };
        var identity = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);
        return AuthenticateResult.Success(ticket);
    }

    protected override async Task HandleChallengeAsync(AuthenticationProperties properties) {
        Response.Headers["WWW-Authenticate"] = $"Basic realm=\"Custom realm name here\", charset=\"UTF-8\"";
        await base.HandleChallengeAsync(properties);
    }
}

I added this to Startup.cs:

.AddScheme<AuthenticationSchemeOptions, CustomAuthenticationHandler>("Test", "Testing", o => { })

The problem is that the HandleAuthenticateAsync method is never called. The other solutions I find to this problem generally say you need to make it the "default" authentication scheme, but I don't want this to interfere with how the other external login providers are set up.

Is there a way I can get this working without changing the default authentication scheme or adding additional attributes to my controllers or actions?

1

There are 1 best solutions below

1
On

You need add a default Scheme when you add AddAuthentication

like services.AddAuthentication("Test")