asp.netauthentication

Custom RemoteAuthenticationHandler won't sign in


I have a bare-bones RemoteAuthenticationHandler that I am going to build on, but I can't work out exactly what HandleRemoteAuthenticateAsync should do for it to work.

using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;

namespace BlazorAuthExample;

public class MyAuthOptions : RemoteAuthenticationOptions
{
    public MyAuthOptions()
    {
        CallbackPath = new PathString("/signin-myauth");
    }
}

public class MyAuthHandler : RemoteAuthenticationHandler<MyAuthOptions>
{
    public MyAuthHandler(
        IOptionsMonitor<MyAuthOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder) : base(options, logger, encoder)
    {
    }

    protected override Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Context.Response.Redirect("/my-sign-in-page");
        return Task.CompletedTask;
    }

    protected override Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
    {
        var nameIdentifierClaim = new Claim(ClaimTypes.NameIdentifier, "mrpmorris@gmail.com", ClaimTypes.NameIdentifier, "myauth", "myauth");
        var identity = new ClaimsIdentity([nameIdentifierClaim], "myauth");
        var principal = new ClaimsPrincipal(identity);

        var ticket = new AuthenticationTicket(principal, "myauth");
        return Task.FromResult(HandleRequestResult.Success(ticket));
    }
}

  1. Create a new Blazor Web App, and specify "Individual Accounts" as the authentication kind.
  2. Add the above class
  3. In Program.cs find
builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddIdentityCookies();

and change it to

builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddRemoteScheme<MyAuthOptions, MyAuthHandler>("myauth", "My auth", null)
    .AddIdentityCookies();
  1. Create a file MySignInPage.razor with the following contents
@page "/my-sign-in-page"
<form action="/signin-myauth" method="post">
   <button type="submit">Click here to sign in</button>
</form>
  1. Run the app
  2. Click the Register item in the nav menu on the left
  3. Under "Use another service to register" click My auth
  4. Click the "Click here to sign in" button

I now expect the same behaviour as I see when I sign into Google et al, but it seems nothing happens at all.


Solution

  • You are not handling RedirectUri in your handler.

    Add those two lines to your HandleRemoteAuthenticateAsync method.

    // hardcoded for sake of simplicity
    ticket.Properties.RedirectUri = "/Account/ExternalLogin?ReturnUrl=&Action=LoginCallback";
    ticket.Properties.Items.Add("LoginProvider", "myauth");
    

    Ideally, you must pass RedirectUri from the challenge method (properties.RedirectUri) to your login page and then to your handle method.

    Updated:

    Following is a sample implementation that supports the proper handling of RedirectUri. Do not use this in your production.

    public class MyAuthHandler : RemoteAuthenticationHandler<MyAuthOptions>
    {
        public MyAuthHandler(
            IOptionsMonitor<MyAuthOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder) : base(options, logger, encoder)
        {
        }
    
        protected override Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            var url = "/my-sign-in-page?RedirectUri=" + Uri.EscapeDataString(properties.RedirectUri ?? "");
    
            if (properties.Items.TryGetValue("XsrfId", out var xsrfId))
            {
                url += "&xsrfId=" + xsrfId;
            }
    
            Context.Response.Redirect(url);
    
            return Task.CompletedTask;
        }
    
        protected override Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
        {
            var nameIdentifierClaim = new Claim(ClaimTypes.NameIdentifier, "mrpmorris@gmail.com", ClaimTypes.NameIdentifier, "myauth", "myauth");
            var identity = new ClaimsIdentity([nameIdentifierClaim], "myauth");
            var principal = new ClaimsPrincipal(identity);
    
            var ticket = new AuthenticationTicket(principal, "myauth");
    
            ticket.Properties.RedirectUri = Context.Request.Form["RedirectUri"];
            ticket.Properties.Items.Add("LoginProvider", "myauth");
            if (Context.Request.Form.TryGetValue("XsrfId", out var xsrfId))
            {
                ticket.Properties.Items.Add("XsrfId", xsrfId);
            }
    
            return Task.FromResult(HandleRequestResult.Success(ticket));
        }
    }
    
    @page "/my-sign-in-page"
    <form action="/signin-myauth" method="post">
        <input type="hidden" name="RedirectUri" value="@RedirectUri"/>
        <input type="hidden" name="XsrfId" value="@XsrfId"/>
        <button type="submit">Click here to sign in</button>
    </form>
    
    @code {
    
        [SupplyParameterFromQuery]
        private string? RedirectUri { get; set; }
    
        [SupplyParameterFromQuery]
        private string? XsrfId { get; set; }
    
    }