Back to feature list...
User authentication
On this page:
When it comes to user authentication, Rebex File Server is versatile. You can either choose to use the simple built-in virtual user database, or easily implement your own authentication provider.
Built-in user database
By default, FileServer
uses a built-in user database provided by its
Users collection to authenticate connected users.
It's very simple and non-persistent - just add one or more virtual users on startup and once you
launch the server,
it will work instantly.
// add some users
server.Users.Add("bob", "hispassword", @"c:\data\bob");
server.Users.Add("alice", "herpassword", @"c:\data\alice");
// this is actually equivalent
var robot = new FileServerUser("robot", "itspassword", @"c:\data\robot");
server.Users.Add(robot);
// removing users is simple as well
server.Users.Remove("bob");
server.Users.Remove(robot);
// and you can list the users easily
foreach (var user in server.Users)
{
Console.WriteLine(user.Name);
}
' add some users
server.Users.Add("bob", "hispassword", "c:\data\bob")
server.Users.Add("alice", "herpassword", "c:\data\alice")
' this is actually equivalent
Dim robot = New FileServerUser("robot", "itspassword", "c:\data\robot")
server.Users.Add(robot)
' removing users is simple as well
server.Users.Remove("bob")
server.Users.Remove(robot)
' and you can list the users easily
For Each serverUser In server.Users
Console.WriteLine(serverUser.Name)
Next
Tip: Instead of using the local file system, Rebex File Server makes it possible to create composite file systems by mounting multiple local directories into desired virtual paths. It supports custom file system providers as well.
Custom authentication provider
When the built-in user database is not sufficient, you can easily implement a custom authentication provider using the
Authentication event.
// register authentication event handler
server.Authentication += (sender, e) =>
{
MyDbUser myUser;
// try authenticating the user against a custom user database
if (MyUserDatabase.TryAuthenticate(
e.UserName, e.Password, out myUser))
{
// construct a user object
var user = new FileServerUser(myUser.UserName, null, myUser.VirtualRoot);
// accept authentication attempt with this user object
e.Accept(user);
}
else
{
// reject authentication attempt
e.Reject();
}
};
' register authentication event handler
AddHandler server.Authentication,
Sub(sender, e)
Dim myUser As MyDbUser = Nothing
' try authenticating the user against a custom user database
If MyUserDatabase.TryAuthenticate(e.UserName, e.Password, myUser) Then
' construct a user object
Dim user = New FileServerUser(myUser.UserName, Nothing, myUser.VirtualRoot)
' accept authentication attempt with this user object
e.Accept(user)
Else
' reject authentication attempt
e.Reject()
End If
End Sub
Authentication
event is registered, authentication against the built-in user database is not performed by default. It's completely up to you
whether you accept or deny an authentication attempt.
The AuthenticationEventArgs
object passed to the event handler contains information such as
UserName, supplied
Password or
Key,
ClientAddress and
ClientEndPoint. It also provides a reference to the
Users object,
making it easily possible to utilize the built-in user database as well.
Username/password authentication
By default, password-based authentication is used for authentication against the built-in user database and for custom authentication providers.
However, it can be disabled using
Settings.AllowedAuthenticationMethods
- for example if public key authentication
is the only authentication method allowed.
Public key authentication (SFTP/SSH only)
Asymmetric cryptography makes it possible for a client to authenticate using a private key without revealing it to the SSH/SFTP/SCP server (or anyone else) - the server only needs to learn about the corresponding public key.
The need to store the public keys at the server complicates things a bit, but public key authentication can still be surprisingly simple. For example, if you only need to authenticate a single user:
// robot's user name
string userName = "robot";
// load robot's public key from a file
var publicKey = new SshPublicKey("robot.pub");
// robot's virtual root path
string virtualRoot = @"c:\data\robot";
// only allow 'public key' authentication
server.Settings.AllowedAuthenticationMethods = AuthenticationMethods.PublicKey;
// register authentication event handler
server.Authentication += (sender, e) =>
{
// make sure that username and public key match the expected values
if (e.UserName == userName && e.Key != null && e.Key.Equals(publicKey))
{
e.Accept(new FileServerUser(e.UserName, null, virtualRoot));
}
else
{
e.Reject();
}
};
' robot's user name
Dim userName As String = "robot"
' load robot's public key from a file
Dim publicKey As New SshPublicKey("robot.pub")
' robot's virtual root path
Dim virtualRoot As String = "c:\data\robot"
' only allow 'public key' authentication
server.Settings.AllowedAuthenticationMethods = AuthenticationMethods.PublicKey
' register authentication event handler
AddHandler server.Authentication,
Sub(sender, e)
' make sure that username and public key match the expected values
If e.UserName = userName AndAlso e.Key IsNot Nothing AndAlso e.Key.Equals(publicKey) Then
e.Accept(New FileServerUser(e.UserName, Nothing, virtualRoot))
Else
e.Reject()
End If
End Sub
Note: When the Authentication
event has been called, it means that the server already made sure that the client is in possession
of the corresponding private key.
Note: For the sake of simplicity, we have been comparing public key fingerprints (hashes) in some of our samples. Please be aware that this is not considered sufficiently secure. Make sure to compare whole public keys in production code.
Check out the Advanced authentication provider section for a slightly more advanced sample code.
Password and public key authentication (SFTP/SSH only)
Authentication using both password and public key is slightly more complicated
due to the nature of the SSH authentication protocol. The credentials are provided by the client sequentially, which means that the
Authentication
event is going to be called twice. During the first call, only accept the credential partially.
// robot's user name
string userName = "robot";
// robot's password
string password = "password";
// load robot's public key from a file
var publicKey = new SshPublicKey("robot.pub");
// robot's virtual root path
string virtualRoot = @"c:\data\robot";
// allow 'public key' and password authentication
server.Settings.AllowedAuthenticationMethods =
AuthenticationMethods.PublicKey | AuthenticationMethods.Password;
// register authentication event handler
server.Authentication += (sender, e) =>
{
if (e.UserName == userName)
{
bool correct = false;
if (e.Key != null)
{
// when authenticating using a key, make sure it is correct
if (e.Key.Equals(publicKey))
{
correct = true;
}
}
else if (e.Password != null)
{
// when authenticating using a password, make sure it is correct
if (e.Password == password)
{
correct = true;
}
}
if (correct)
{
if (e.PartiallyAccepted)
{
// if another kind of credential has already been provided
// and accepted, authenticate the user
e.Accept(new FileServerUser(e.UserName, null, virtualRoot));
return;
}
else
{
// if another kind of credential has not been provided yet,
// only accept the current one partially
e.AcceptPartially();
return;
}
}
}
// if no correct credential was supplied, reject this authentication attempt
e.Reject();
};
' robot's user name
Dim userName As String = "robot"
' robot's password
Dim password As String = "password"
' load robot's public key from a file
Dim publicKey As New SshPublicKey("robot.pub")
' robot's virtual root path
Dim virtualRoot As String = "c:\data\robot"
' allow 'public key' and password authentication
server.Settings.AllowedAuthenticationMethods = AuthenticationMethods.PublicKey Or AuthenticationMethods.Password
' register authentication event handler
AddHandler server.Authentication,
Sub(sender, e)
' make sure that username and public key match the expected values
If e.UserName = userName Then
Dim correct As Boolean = False
If e.Key IsNot Nothing Then
' when authenticating using a key, make sure it is correct
If e.Key.Equals(publicKey) Then correct = True
ElseIf e.Password IsNot Nothing Then
' when authenticating using a password, make sure it is correct
If e.Password = password Then correct = True
End If
If correct Then
If e.PartiallyAccepted Then
' if another kind of credential has already been provided
' and accepted, authenticate the user
e.Accept(New FileServerUser(e.UserName, Nothing, virtualRoot))
Return
Else
' if another kind of credential has not been provided yet,
' only accept the current one partially
e.AcceptPartially()
Return
End If
End If
End If
' if no correct credential was supplied, reject this authentication attempt
e.Reject()
End Sub
X.509 certificate authentication
Client authentication using X.509 certificates is supported as well. This is not commonly used for SFTP/SSH protocol. On the other hand, X.509 certificates are often with FTP/TLS protocol for client authentication.
// user certificate's thumbprint
var thumbprint = "3977D2EA9992A83D583FDF739D56915CC06EF5EB";
// instruct server to use client certificates
server.Settings.TlsParameters.CertificatePolicy = TlsCertificatePolicy.AllowClientCertificate;
// register authentication event handler
server.Authentication += (sender, e) =>
{
// make sure that username and certificate match the expected values
if (e.UserName == username && e?.Certificate?.LeafCertificate?.Thumbprint == thumbprint)
{
e.Accept(new FileServerUser(e.UserName, null, @"c:\data\user01"));
}
else
{
e.Reject();
}
};
' load the user's certificate from a file
Dim thumbprint = "3977D2EA9992A83D583FDF739D56915CC06EF5EB"
' instruct server to use client certificates
server.Settings.TlsParameters.CertificatePolicy = TlsCertificatePolicy.AllowClientCertificate
' register authentication event handler
AddHandler server.Authentication,
Sub(sender, e)
' make sure that username And certificate match the expected values
If e.UserName = username And e?.Certificate?.LeafCertificate?.Thumbprint = thumbprint Then
e.Accept(New FileServerUser(e.UserName, Nothing, "c:\data\user01"))
Else
e.Reject()
End If
End Sub
Advanced authentication provider
In many cases, it's easier to reuse the built-in user database instead of writing your own. For example, if all you need is to add public key authentication support, you might consider the following approach.
First, extend the built-in
FileServerUser object:
public class UserWithKey : FileServerUser
{
public SshPublicKey Key { get; private set; }
public UserWithKey(string userName, SshPublicKey key, string physicalRootPath)
: base(userName, null, physicalRootPath)
{
Key = key;
}
}
Public Class UserWithKey
Inherits FileServerUser
Public Property Key() As SshPublicKey
Get
Return m_Key
End Get
Private Set(value As SshPublicKey)
m_Key = value
End Set
End Property
Private m_Key As SshPublicKey
Public Sub New(userName As String, sshKey As SshPublicKey, physicalRootPath As String)
MyBase.New(userName, Nothing, physicalRootPath)
Key = sshKey
End Sub
End Class
Then, add some users and implement an authentication provider that supports both password-based and key-based authentication methods:
// allow 'password' and 'public key' authentication
server.Settings.AllowedAuthenticationMethods =
AuthenticationMethods.PublicKey | AuthenticationMethods.Password;
// add users that use passwords
server.Users.Add("bob", "hispassword", @"c:\data\bob");
server.Users.Add("alice", "herpassword", @"c:\data\alice");
// add users that use public keys
var robotKey = new SshPublicKey("robot.pub");
server.Users.Add(new UserWithKey("robot", robotKey, @"c:\data\robot"));
var serviceKey = new SshPublicKey("service.pub");
server.Users.Add(new UserWithKey("service", serviceKey, @"c:\data\service"));
// register authentication event handler
server.Authentication += (sender, e) =>
{
// make sure the user exists
FileServerUser user = server.Users[e.UserName];
if (user != null)
{
// if it's a 'public key' user, check the key
var userWithKey = user as UserWithKey;
if (userWithKey != null && userWithKey.Key.Equals(e.Key))
{
e.Accept(userWithKey);
return;
}
// otherwise, check the password
if (user.CheckPassword(e.Password))
{
e.Accept(user);
return;
}
}
e.Reject();
};
' allow 'password' and 'public key' authentication
server.Settings.AllowedAuthenticationMethods = AuthenticationMethods.PublicKey Or AuthenticationMethods.Password
' add users that use passwords
server.Users.Add("bob", "hispassword", "c:\data\bob")
server.Users.Add("alice", "herpassword", "c:\data\alice")
' add users that use public keys
Dim robotKey = New SshPublicKey("robot.pub")
server.Users.Add(New UserWithKey("robot", robotKey, "c:\data\robot"))
Dim serviceKey = New SshPublicKey("service.pub")
server.Users.Add(New UserWithKey("service", serviceKey, "c:\data\service"))
' register authentication event handler
AddHandler server.Authentication,
Sub(sender, e)
' make sure the user exists
Dim user As FileServerUser = server.Users(e.UserName)
If user IsNot Nothing Then
' if it's a 'public key' user, check the key
Dim userWithKey = TryCast(user, UserWithKey)
If userWithKey IsNot Nothing AndAlso userWithKey.Key.Equals(e.Key) Then
e.Accept(userWithKey)
Exit Sub
End If
' otherwise, check the password
If user.CheckPassword(e.Password) Then
e.Accept(user)
Exit Sub
End If
End If
e.Reject()
End Sub
In addition to this, you might want to register an additional
PreAuthentication
event. It's called when authentication is just about to start
and makes it possible to specify the authentication methods that are allowed for the user attempting authentication. In this case, we only
allow 'public key' authentication when appropriate, preventing other users from even attempting key-based authentication:
// register pre-authentication event handler
server.PreAuthentication += (sender, e) =>
{
// determine which authentication methods to allow based on the user name
FileServerUser user = server.Users[e.UserName];
if (user is UserWithKey)
{
// only allow public key authentication for key-only users
e.Accept(AuthenticationMethods.PublicKey);
}
else
{
// only allow password authentication for the rest
// (even if the user doesn't exist, pretend it does)
e.Accept(AuthenticationMethods.Password);
}
};
' register pre-authentication event handler
AddHandler server.PreAuthentication,
Sub(sender, e)
' determine which authentication methods to allow based on the user name
Dim user As FileServerUser = server.Users(e.UserName)
If TypeOf user Is UserWithKey Then
' only allow public key authentication for key-only users
e.Accept(AuthenticationMethods.PublicKey)
Else
' only allow password authentication for the rest
' (even if the user doesn't exist, pretend it does)
e.Accept(AuthenticationMethods.Password)
End If
End Sub
Changing password
FileServer provides the
ChangePassword
event that is raised when a client attempts to change their password.
Handling that event, you can change the password in your user database.
// register ChangePassword event
server.ChangePassword += (sender, e) =>
{
// retrieve the user from the built-in user database
var user = server.Users[e.UserName];
if (user == null)
{
e.CustomMessage = "Unknown user.";
e.Reject();
return;
}
// verify the old password
if (!user.CheckPassword(e.OldPassword))
{
e.CustomMessage = "Old password does not match.";
e.Reject();
return;
}
// change the password
user.SetPassword(e.NewPassword);
e.Accept();
};
' register ChangePassword event
AddHandler server.ChangePassword, Sub(sender, e)
' retrieve the user from the built-in user database
Dim user = server.Users(e.UserName)
If user Is Nothing Then
e.CustomMessage = "Unknown user."
e.Reject()
Return
End If
' verify the old password
If Not user.CheckPassword(e.OldPassword) Then
e.CustomMessage = "Old password does not match."
e.Reject()
Return
End If
' change the password
user.SetPassword(e.NewPassword)
e.Accept()
End Sub
Back to feature list...