Search code examples
c#asp.net-coreasp.net-core-authenticationhandler

RemoteAuthenticationHandler state and code_challenge verification


I have created RemoteAuthenticationHandler, which looks like that:

public class AuthAndAuthHandler : RemoteAuthenticationHandler<AuthAndAuthSchemeOptions>

{
    public AuthAndAuthHandler(IOptionsMonitor<AuthAndAuthSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        var rng = RandomNumberGenerator.Create();

        var state = new byte[128];
        var nonce = new byte[128];
        var codeVerifier = new byte[64];

        rng.GetBytes(state);
        rng.GetBytes(nonce);
        rng.GetBytes(codeVerifier);

        var codeChallenge = SHA256.HashData(codeVerifier);

        Response.Cookies.Append("Nonce", Convert.ToBase64String(SHA256.HashData(nonce)), new CookieOptions
        {
            Path = "/callback",
            HttpOnly = true,
            IsEssential = true,
            Secure = true,
            SameSite = SameSiteMode.Strict,
            Expires = Clock.UtcNow.AddHours(1)
        });

        Response.Redirect($"{Options.Authority}/authorization?client_id={Options.ClientId}" +
            $"&callback_uri={Request.Scheme}://{Request.Host}{Options.CallbackPath}&scopes={Options.Scopes}" +
            $"&state={Convert.ToBase64String(state)}&nonce={Convert.ToBase64String(nonce)}&code_challenge={Convert.ToBase64String(codeChallenge)}");
    }

    protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
    {
        throw new NotImplementedException();
    }
}

And in HandleRemoteAuthenticateAsync() method I have to verify state, which I will get after successful remote authorization. How can I do this, when after Challenge I'm losing earlier generated state and code verifier?


Solution

  • According to state, you can validate it with Microsoft.AspNetCore.Authentication.ISecureDataFormat<TData>. It can be defined as property inside your custom options (pseudocode):

    public class CustomRemoteOptions : RemoteAuthenticationOptions
    {
        public CustomRemoteOptions()
        {
            var dataProtector = DataProtectionProvider.CreateProtector(
                typeof(CustomRemoteHandler).FullName);
            StateDataFormat = new PropertiesDataFormat(dataProtector);
        }
    
        internal ISecureDataFormat<AuthenticationProperties> StateDataFormat { get; }
    }
    

    Then use it inside CustomRemoteHandler:

    public class CustomRemoteHandler : RemoteAuthenticationHandler<CustomRemoteOptions>
    {
        protected override Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            string state = Options.StateDataFormat.Protect(properties);
            // Then add the state to the URI.
        }
        
        protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync()
        {
            var state = Request.Query["state"];
        
            var properties = Options.StateDataFormat.Unprotect(state);
            if (properties == null)
            {
                return HandleRequestResult.Fail("The Custom authentication state was missing or invalid.");
            }
        }
    }
    

    Note: if you also need to validate correlation id, then add GenerateCorrelationId(properties); inside HandleChallengeAsync and the following lines inside HandleRemoteAuthenticateAsync:

    if (!ValidateCorrelationId(properties))
    {
        return HandleRequestResult.Fail("Validation of correlation failed.", properties);
    }
    

    Note: to make the code testable you can move initialization of StateDataFormat to custom IPostConfigureOptions implementation.