RemoteAuthenticationHandler state and code_challenge verification

549 views Asked by At

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?

1

There are 1 answers

0
Ales Kazakevich On BEST ANSWER

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.