Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Src/SmtpServer.Tests/MailClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ public static void Send(
client.Disconnect(true);
}

#nullable enable
public static void Send(
SaslMechanism saslMechanism,
MimeMessage? message = null!
)
{
message ??= Message();

System.ArgumentNullException.ThrowIfNull(message);
System.ArgumentNullException.ThrowIfNull(saslMechanism);

using var client = Client();

client.Authenticate(saslMechanism);

//client.NoOp();

client.Send(message);
client.Disconnect(true);
}
#nullable restore

public static void NoOp(SecureSocketOptions options = SecureSocketOptions.Auto)
{
using var client = Client(options: options);
Expand Down
64 changes: 64 additions & 0 deletions Src/SmtpServer.Tests/SmtpParserTests.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Claims;
using System.Text;
using SmtpServer.Mail;
using SmtpServer.Protocol;
using SmtpServer.Text;
using Xunit;
using SecurityAlgorithms = Microsoft.IdentityModel.Tokens.SecurityAlgorithms;
using SigningCredentials = Microsoft.IdentityModel.Tokens.SigningCredentials;
using SymmetricSecurityKey = Microsoft.IdentityModel.Tokens.SymmetricSecurityKey;

namespace SmtpServer.Tests
{
Expand Down Expand Up @@ -149,6 +154,44 @@ public void CanMakeAuthLogin()
Assert.Equal("Y2Fpbi5vc3VsbGl2YW5AZ21haWwuY29t", ((AuthCommand)command).Parameter);
}

[Fact]
public void CanMakeAuthXOAuth2()
{
// arrange
string email = "test-user@host.com";
string token = GenerateJwt(email, "my-very-long-at-least-32-bytes-key");
string authString = $"user={email}\u0001auth=Bearer {token}\u0001\u0001";

var reader = CreateReader($"AUTH XOAUTH2 {Convert.ToBase64String(Encoding.UTF8.GetBytes(authString))}");

// act
var result = Parser.TryMakeAuth(ref reader, out var command, out var errorResponse);

// assert
Assert.True(result);
Assert.True(command is AuthCommand);
Assert.Equal(AuthenticationMethod.XOAuth2, ((AuthCommand) command).Method);
}

[Fact]
public void CanMakeAuthOAuthBearer()
{
// arrange
string email = "test-user@host.com";
string token = GenerateJwt(email, "my-very-long-at-least-32-bytes-key");
string authString = $"user={email}\u0001auth=Bearer {token}\u0001\u0001";

var reader = CreateReader($"AUTH OAUTHBEARER {Convert.ToBase64String(Encoding.UTF8.GetBytes(authString))}");

// act
var result = Parser.TryMakeAuth(ref reader, out var command, out var errorResponse);

// assert
Assert.True(result);
Assert.True(command is AuthCommand);
Assert.Equal(AuthenticationMethod.OAuthBearer, ((AuthCommand) command).Method);
}

[Theory]
[InlineData("MAIL FROM:<cain.osullivan@gmail.com>", "cain.osullivan", "gmail.com")]
[InlineData(@"MAIL FROM:<""Abc@def""@example.com>", "Abc@def", "example.com")]
Expand Down Expand Up @@ -688,5 +731,26 @@ public void CanNotMakeIPv6AddressLiteral(string input)
// assert
Assert.False(result);
}

static string GenerateJwt(string nameId, string key)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

var handler = new JwtSecurityTokenHandler();
var token = handler.CreateJwtSecurityToken(
issuer: "test-issuer",
audience: "smtp",
subject: new ClaimsIdentity(
[
new Claim(ClaimTypes.NameIdentifier, nameId),
]),
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.AddMinutes(10),
signingCredentials: credentials
);

return handler.WriteToken(token);
}
}
}
1 change: 1 addition & 0 deletions Src/SmtpServer.Tests/SmtpServer.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ItemGroup>
<PackageReference Include="MailKit" Version="4.7.1.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.13.0" />
<PackageReference Include="xunit" Version="2.9.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
Expand Down
105 changes: 96 additions & 9 deletions Src/SmtpServer.Tests/SmtpServerTests.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
using MailKit;
using SmtpServer.Authentication;
using SmtpServer.ComponentModel;
using SmtpServer.Mail;
using SmtpServer.Net;
using SmtpServer.Protocol;
using SmtpServer.Storage;
using SmtpServer.Tests.Mocks;
using System;
using System;
using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MailKit;
using MailKit.Security;
using Microsoft.IdentityModel.Tokens;
using SmtpServer.Authentication;
using SmtpServer.ComponentModel;
using SmtpServer.Mail;
using SmtpServer.Net;
using SmtpServer.Protocol;
using SmtpServer.Storage;
using SmtpServer.Tests.Mocks;
using Xunit;
using SmtpResponse = SmtpServer.Protocol.SmtpResponse;

Expand Down Expand Up @@ -95,6 +99,68 @@ public void CanAuthenticateUser()
}
}

#nullable enable
[Fact]
public void CanAuthenticateUser_WithXOAuth2()
{
// arrange
string? actualUser = null;
string? actualBearerToken = null;

var bearerTokenAuthenticator = new DelegatingBearerTokenAuthenticator((u, bt) =>
{
actualUser = u;
actualBearerToken = bt;

return true;
});

string nameIdentifier = "user@host.com";
string bearerToken = GenerateJwt(nameIdentifier, "my-very-long-at-least-32-bytes-key");

using (CreateServer(endpoint => endpoint.AllowUnsecureAuthentication(), services => services.Add(bearerTokenAuthenticator)))
{
// act
MailClient.Send(new SaslMechanismOAuth2(nameIdentifier, bearerToken));

// assert
Assert.Single(MessageStore.Messages);
Assert.Equal(nameIdentifier, actualUser);
Assert.Equal(bearerToken, actualBearerToken);
}
}

[Fact]
public void CanAuthenticateUser_WithOAuthBearer()
{
// arrange
string? actualUser = null;
string? actualBearerToken = null;

var bearerTokenAuthenticator = new DelegatingBearerTokenAuthenticator((u, bt) =>
{
actualUser = u;
actualBearerToken = bt;

return true;
});

string nameIdentifier = "user@host.com";
string bearerToken = GenerateJwt(nameIdentifier, "my-very-long-at-least-32-bytes-key");

using (CreateServer(endpoint => endpoint.AllowUnsecureAuthentication(), services => services.Add(bearerTokenAuthenticator)))
{
// act
MailClient.Send(new SaslMechanismOAuthBearer(nameIdentifier, bearerToken));

// assert
Assert.Single(MessageStore.Messages);
Assert.Equal(nameIdentifier, actualUser);
Assert.Equal(bearerToken, actualBearerToken);
}
}
#nullable restore

[Theory]
[InlineData("", "")]
[InlineData("user", "")]
Expand Down Expand Up @@ -639,5 +705,26 @@ SmtpServerDisposable CreateServer(
/// The cancellation token source for the test.
/// </summary>
public CancellationTokenSource CancellationTokenSource { get; }

static string GenerateJwt(string nameId, string key)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

var handler = new JwtSecurityTokenHandler();
var token = handler.CreateJwtSecurityToken(
issuer: "test-issuer",
audience: "smtp",
subject: new ClaimsIdentity(
[
new Claim(ClaimTypes.NameIdentifier, nameId),
]),
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.AddMinutes(10),
signingCredentials: credentials
);

return handler.WriteToken(token);
}
}
}
50 changes: 50 additions & 0 deletions Src/SmtpServer/Authentication/BearerTokenAuthenticator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Threading;
using System.Threading.Tasks;

namespace SmtpServer.Authentication
{
/// <summary>
/// Bearer Token Authenticator
/// </summary>
public abstract class BearerTokenAuthenticator : IBearerTokenAuthenticator
{
/// <summary>
/// Default Bearer Token Authenticator
/// </summary>
public static readonly IBearerTokenAuthenticator Default = new DefaultBearerTokenAuthenticator();

/// <summary>
/// Authenticate a user account utilizing a bearer token.
/// </summary>
/// <param name="context">The session context.</param>
/// <param name="user">The user to authenticate.</param>
/// <param name="bearerToken">The bearer token of the user.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>true if the user is authenticated, false if not.</returns>
public abstract Task<bool> AuthenticateAsync(
ISessionContext context,
string user,
string bearerToken,
CancellationToken cancellationToken);

sealed class DefaultBearerTokenAuthenticator : BearerTokenAuthenticator
{
/// <summary>
/// Authenticate a user account utilizing a bearer token.
/// </summary>
/// <param name="context">The session context.</param>
/// <param name="user">The user to authenticate.</param>
/// <param name="bearerToken">The bearer token of the user.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>true if the user is authenticated, false if not.</returns>
public override Task<bool> AuthenticateAsync(
ISessionContext context,
string user,
string bearerToken,
CancellationToken cancellationToken)
{
return Task.FromResult(true);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace SmtpServer.Authentication
{
/// <summary>
/// Delegating BearerToken Authenticator
/// </summary>
public sealed class DelegatingBearerTokenAuthenticator : BearerTokenAuthenticator
{
readonly Func<ISessionContext, string, string, bool> _delegate;

/// <summary>
/// Constructor.
/// </summary>
/// <param name="delegate">THe delegate to execute for the authentication.</param>
public DelegatingBearerTokenAuthenticator(Action<string, string> @delegate) : this(Wrap(@delegate)) { }

/// <summary>
/// Constructor.
/// </summary>
/// <param name="delegate">THe delegate to execute for the authentication.</param>
public DelegatingBearerTokenAuthenticator(Func<string, string, bool> @delegate) : this(Wrap(@delegate)) { }

/// <summary>
/// Constructor.
/// </summary>
/// <param name="delegate">THe delegate to execute for the authentication.</param>
public DelegatingBearerTokenAuthenticator(Func<ISessionContext, string, string, bool> @delegate)
{
_delegate = @delegate;
}

/// <summary>
/// Wrap the delegate into a function that is compatible with the signature.
/// </summary>
/// <param name="delegate">The delegate to wrap.</param>
/// <returns>The function that is compatible with the main signature.</returns>
static Func<ISessionContext, string, string, bool> Wrap(Func<string, string, bool> @delegate)
{
return (context, bearerToken, password) => @delegate(bearerToken, password);
}

/// <summary>
/// Wrap the delegate into a function that is compatible with the signature.
/// </summary>
/// <param name="delegate">The delegate to wrap.</param>
/// <returns>The function that is compatible with the main signature.</returns>
static Func<ISessionContext, string, string, bool> Wrap(Action<string, string> @delegate)
{
return (context, bearerToken, password) =>
{
@delegate(bearerToken, password);

return true;
};
}

/// <summary>
/// Authenticate a bearerToken account.
/// </summary>
/// <param name="context">The session context.</param>
/// <param name="bearerToken">The bearerToken to authenticate.</param>
/// <param name="password">The password of the bearerToken.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>true if the bearerToken is authenticated, false if not.</returns>
public override Task<bool> AuthenticateAsync(
ISessionContext context,
string bearerToken,
string password,
CancellationToken cancellationToken)
{
return Task.FromResult(_delegate(context, bearerToken, password));
}
}
}
Loading