Handle SFTP Server events with async code

+1 vote
asked Jul 7 by jwick (220 points)

Hello,

I'm exploring the guidance in Custom authentication provider to implement some custom auth logic using the PreAuthentication and Authentication event handlers, but my logic requires calling into external services that require calling async code.

The trouble is because it's async called by sync event handler, the Server thinks my response is complete before my logic is even run. Any guidance on how to address this? I assume you don't have some alternative async approach to the Authentication even handler?

1 Answer

0 votes
answered Jul 8 by renestein (3,870 points)
selected Jul 8 by jwick
 
Best answer

Hi jwick,
thanks for the question.
All SFTP server events are synchronous now. We probably redesign events when we will have async SSH core.

When another library provides only async API, you have to use the "sync over async" call as the smallest evil. We are aware that "sync over async" call is far from ideal and may cause problems when your server is under heavy load (using more threads than necessary, thread pool starvation).

Please try the following code.

          //Handle Authentication event to authenticate users against the database. Don't use await in the **async void** event handler, delegate to Task-based method and Wait for the result. 
    _sftpServer.Authentication += (sender, args) => SftpServerAuthentication(sender, args).Wait();

//Always use ConfigureAwait(false) in the 'await' expression.

    /// <summary>
    /// Handler for the <see cref="Server.Authentication"/> event.
    /// </summary>
    private static async Task SftpServerAuthentication(object sender, AuthenticationEventArgs e)
    {
      //Create 'anonymous' FileSystemDbContext for administrative task - user authentication.
      using var context = new FileSystemDbContext(_options);

      //Read user name from AuthenticationEventArgs.
      var currentUserName = e.UserName;

      //Load a user from database.  
      var user = await context.GetUserByName(currentUserName).ConfigureAwait(false);

      //When a user is not in our database, deny access.
      if (user == null)
      {
        e.Reject();
        return;
      }

      //Verify user password
      if (!_passwordHashService.VerifyPassword(user, e.Password))
      {
        //When user sends invalid password, deny access.
        e.Reject();
        return;
      }

      //Create "user session scoped" instance of the FileSystemDbContext.
      var fileSystemDbContext = new FileSystemDbContext(_options, currentUserName);
      //Create "user session scoped" instance of the EfFileSystemProvider.
      var efFileSystemProvider = new EfFileSystemProvider(fileSystemDbContext);
      //Create instance of our DbFileServerUser with user specific file system.
      var dbFileServerUser = new DbFileServerUser(currentUserName, efFileSystemProvider);

      //Authentication succeeded. Allow user logon.
      e.Accept(dbFileServerUser);
    }
commented Jul 8 by jwick (220 points)
Thanks for the response, I was "afraid" you would say that ;-)

But at least I have confirmation and certainty there isn't currently a better approach.

For now I'm playing around with using the .Wait(timeout) variation to limit the possibility of a lock up, since most authentication attempts should be completed within a minute or two.
commented Jul 8 by renestein (3,870 points)
Thanks for letting me know.

Yes, Wait method with timeout is certainly better.
Unfortunately, there is no simple solution for bridging the gap between the sync and the async world, and only tweaks like support for the timeout or cancellation are possible.
It is also possible to 'hijack' the already waiting thread in the event handler and marshal all continuations from thread pool threads to this thread using the custom synchronization context. I am afraid that the solution would be very complex and still does not solve the main issue.
...