This article is part of The Third Annual C# Advent. You'll find other helpful articles and tutorials published daily by community members and experts there, so make sure to check it out every day.
Passwords are the most common user-authentication methods to grant access to a (digital) service. Trying to guess anyone's password is more common than you might think... and requires not so much effort (yikes!) because:
- Users often use the same password for all their services.
- Social engineering, phishing tools, xploits, and other techniques allow attackers to get access into our accounts.
Enter Two-Factor Authentication
2FA is a tool that adds an extra security layer during the login process: When users signs in, they are asked to verify the account ownership. Simply put, they need to provide two different factors:
- The first one is the password.
- The second one can be one of several things: a pin code (sent to a mobile phone), a verification link (sent to an email address)...
In essence, 2FA is reduced to the following: if you want to access a service, you must "know something" and "own something". Thus, 2FA forces an attacker not only to decipher a password but also to (somehow) get access to the second factor, a tough task, as it would require to steal a cellphone for instance. While 2FA is not a guaranteed, foolproof method, it is an excellent barrier to prevent unwanted security breaches.
2FA in software development
As software developers, we should consider 2FA support in our implementations in order to provide additional protection to users. Always? Well, it depends. The rule of thumb is to determine if the information is really valuable for the user. If so, then we should integrate this functionality.
Twilio
Twilio is a Cloud communications Platform as a Service (CPaaS) that allows software developers programmatically to integrate communication functions (such as text/voice messages, video calls, WhatsApp messages, and many more!) using its web service APIs and SDKs available in several technologies, including -but not limited to- PHP, Ruby, and C#/.NET.
Authy
Authy is a Twilio service that simplifies 2FA integration in our apps through a robust API and SDKs without negatively impacting the user experience.
Authy can implement 2FA in several flavors:
- SMS/Voice One-Time-Passwords
- Push Authentications
- API Soft Tokens (Time-based One-Time-Password, TOTP)
Let's do this
We'll use the Authy API and C# to incorporate 2FA functionality to a cross-platform mobile app written in Xamarin. The beauty of C# is that you can use this same code for any other type of project! Want to add 2FA in a ASP .NET Core web app? Use this code! Do you need it for a WPF desktop app? Sure you can, no effort needed! That's awesome!
Alright then. The first step is to create an Authy Application
Once the app is created, you can get the Production API Key that we'll use to establish a secure communication between our app and the service.
All Authy API requests are sent to the base URL https://api.authy.com. It is mandatory to specify the format, either json or xml in the requests. The Authy workflow to complete a basic 2FA verified by SMS consists of 3 steps:
- Create an Authy user
- Send an SMS with the OTP
- Verify the token
So let's add a Constants class in your project, with the code that sums up the previous paragraph (don't forget to replace AuthyAPIKey with your production key ):
public static class Constants
{
public readonly static string AuthyAPIKey = "your-production-key";
public readonly static string AuthyBaseURL = "https://api.authy.com/";
public readonly static string AddUserURL = "protected/json/users/new";
public readonly static string SendOTPURL = "protected/json/sms";
public readonly static string VerifyTokenURL = "protected/json/verify";
}
Any request to the /protected endpoint is secured with the Production API Key by including it in an X-Authy-API-Key HTTP header. Create an AuthyService class with the following initial code:
public class AuthyService
{
private static readonly HttpClient client = CreateHttpClient();
private static HttpClient CreateHttpClient()
{
var client = new HttpClient();
client.BaseAddress = new Uri(Constants.AuthyBaseURL);
client.DefaultRequestHeaders.Add("X-Authy-API-Key", Constants.AuthyAPIKey);
return client;
}
// more code to be added in the next steps...
}
The first step in the 2FA workflow is to Create an Authy user. Generate a new class file, AuthyResponses, which will contain several classes for communication to/from Authy API. The first two are AuthyUserResponse and AuthyUser.
public class AuthyUserResponse
{
public string Message { get; set; }
public AuthyUser User { get; set; }
public bool Success { get; set; }
}
public class AuthyUser
{
public long Id { get; set; }
}
Back into AuthyService class, we implement the AddUser method:
public static async Task<AuthyUserResponse> AddUser(CompanyUser user)
{
var requestContent = new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("user[email]", user.Email),
new KeyValuePair<string, string>("user[cellphone]", user.Cellphone),
new KeyValuePair<string, string>("user[country_code]", user.CountryCode),
});
var response = await client.PostAsync(Constants.AddUserURL, requestContent);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var authyUser = JsonConvert.DeserializeObject<AuthyUserResponse>(content);
return authyUser;
}
return default;
}
The idea is simple. When the user sends their credentials (and they are correct), the app will create/update an AuthyUser which will be used in later phases of the 2FA workflow. Take a look at the json response when step 1 is completed:
Great! So now let's proceed to Step 2: Send an SMS with the OTP. After the user signs in, they need to demonstrate that they are who they claim to be. Besides their credentials, we are also asking them to provide a cellphone on the Log In screen (of course, you won't do this in a real app, it should be done on a settings screen for instance, but this is a demo, so we are ok).
In the AuthyResponses file add an AuthyOTPResponse class:
public class AuthyOTPResponse
{
public bool Success { get; set; }
public string Message { get; set; }
public string Cellphone { get; set; }
}
And the method SendOTP is included in our AuthyService class:
public static async Task<AuthyOTPResponse> SendOTP(long authyID)
{
var response = await client.GetAsync($"{Constants.SendOTPURL}/{authyID}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var authyOTP = JsonConvert.DeserializeObject<AuthyOTPResponse>(content);
return authyOTP;
}
return default;
}
Immediately after the successful credentials input, the user will receive a message on their phone. The json response goes like this:
The third and last step in our 2FA workflow is Verify the token: The user types the received pin code and sends this input back to the service for verification. If it is correct, they have successfully passed the 2FA and we can grant this user access to the app.
First things first. In the AuthyResponses file incorporate two more classes, AuthyVerifyResponse and AuthyDevice:
public class AuthyVerifyResponse
{
public string Message { get; set; }
public string Token { get; set; }
public string Success { get; set; }
public AuthyDevice Device { get; set; }
}
public class AuthyDevice
{
public string City { get; set; }
public string Country { get; set; }
public string Ip { get; set; }
public string Region { get; set; }
public string Registration_city { get; set; }
public string Registration_country { get; set; }
public int? Registration_device_id { get; set; }
public string Registration_ip { get; set; }
public string Registration_method { get; set; }
public string Registration_region { get; set; }
public string Os_type { get; set; }
public object Last_account_recovery_at { get; set; }
public int? Id { get; set; }
public int? Registration_date { get; set; }
}
Secondly, implement the VerifyToken in the AuthyService class:
public static async Task<AuthyVerifyResponse> VerifyToken(long token, long authyID)
{
var response = await client.GetAsync($"{Constants.VerifyTokenURL}/{token}/{authyID}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var authyVerify = JsonConvert.DeserializeObject<AuthyVerifyResponse>(content);
return authyVerify;
}
return default;
}
And that's it! A successful 2FA validation will send the following response:
Depending on the project type, implementation of user interface and service calls might differ. For the sake of completion of the Xamarin app, take a look at the SignUpViewModel class which makes the service calls through Commands:
using System.Windows.Input;
using System.Threading.Tasks;
using Demo2FA.Models;
using Demo2FA.Services;
using Xamarin.Forms;
namespace Demo2FA.ViewModels
{
public class SignUpViewModel : BaseViewModel
{
private CompanyUser _companyUser;
public CompanyUser CompanyUser
{
get { return _companyUser; }
set { _companyUser = value; OnPropertyChanged(); }
}
private AuthyUserResponse _authyUserResponse;
public AuthyUserResponse AuthyUserResponse
{
get { return _authyUserResponse; }
set { _authyUserResponse = value; OnPropertyChanged(); }
}
private AuthyOTPResponse _authyOTPResponse;
public AuthyOTPResponse AuthyOTPResponse
{
get { return _authyOTPResponse; }
set { _authyOTPResponse = value; OnPropertyChanged(); }
}
private AuthyVerifyResponse _authyVerifyResponse;
public AuthyVerifyResponse AuthyVerifyResponse
{
get { return _authyVerifyResponse; }
set { _authyVerifyResponse = value; OnPropertyChanged(); }
}
private long _token;
public long Token
{
get { return _token; }
set { _token = value; OnPropertyChanged(); }
}
private bool _needsVerification;
public bool NeedsVerification
{
get { return _needsVerification; }
set { _needsVerification = value; OnPropertyChanged(); }
}
public ICommand SignUpCommand { private set; get; }
public ICommand VerifyTokenCommand { private set; get; }
public SignUpViewModel()
{
SignUpCommand = new Command(async () => await SignUp());
VerifyTokenCommand = new Command(async () => await VerifyToken());
CompanyUser = new CompanyUser();
}
async Task SignUp()
{
AuthyUserResponse = await AuthyService.AddUser(CompanyUser);
if (AuthyUserResponse != null)
{
AuthyOTPResponse = await AuthyService.SendOTP(AuthyUserResponse.User.Id);
if (AuthyOTPResponse != null)
NeedsVerification = AuthyOTPResponse.Success;
}
}
async Task VerifyToken()
{
AuthyVerifyResponse = await AuthyService.VerifyToken(Token, AuthyUserResponse.User.Id);
}
}
}
Next (and final part) is the UI, written in XAML:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Demo2FA.ViewModels"
xmlns:pancakeView="clr-namespace:Xamarin.Forms.PancakeView;assembly=Xamarin.Forms.PancakeView"
xmlns:custom="clr-namespace:Demo2FA.CustomControls"
mc:Ignorable="d"
x:Class="Demo2FA.Views.SignUpView"
Visual="Material"
BackgroundColor="#7698f3"
Title="Sign Up">
<ContentPage.BindingContext>
<vm:SignUpViewModel/>
</ContentPage.BindingContext>
<ContentPage.Content>
<Grid HorizontalOptions="Center"
VerticalOptions="Center"
Margin="10" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<pancakeView:PancakeView Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
HeightRequest="40" BackgroundColor="Transparent" CornerRadius="5"
BorderThickness="1" BorderColor="White" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image HorizontalOptions="End"
VerticalOptions="Center"
Grid.Column="0"
Aspect="AspectFill">
<Image.Source>
<FontImageSource
FontFamily="{StaticResource FontAwesomeRegular}"
Glyph="{x:Static custom:FontAwesomeIcons.Envelope}"
Color="White"
Size="20" />
</Image.Source>
</Image>
<custom:CustomEntry BackgroundColor="Transparent"
TextColor="White"
PlaceholderColor="White"
VerticalOptions="Center"
HorizontalOptions="Fill"
Placeholder="Name"
Text="{Binding CompanyUser.Name}"
Grid.Column="1" />
</Grid>
</pancakeView:PancakeView>
<pancakeView:PancakeView Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
HeightRequest="40" BackgroundColor="Transparent" CornerRadius="5"
BorderThickness="1" BorderColor="White" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image HorizontalOptions="End"
VerticalOptions="Center"
Grid.Column="0"
Aspect="AspectFill">
<Image.Source>
<FontImageSource
FontFamily="{StaticResource FontAwesomeRegular}"
Glyph="{x:Static custom:FontAwesomeIcons.Envelope}"
Color="White"
Size="20" />
</Image.Source>
</Image>
<custom:CustomEntry BackgroundColor="Transparent"
TextColor="White"
PlaceholderColor="White"
VerticalOptions="Center"
HorizontalOptions="Fill"
Placeholder="Email"
Text="{Binding CompanyUser.Email}"
Grid.Column="1" />
</Grid>
</pancakeView:PancakeView>
<pancakeView:PancakeView Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
HeightRequest="40" BackgroundColor="Transparent" CornerRadius="5"
BorderThickness="1" BorderColor="White" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="3*" />
</Grid.ColumnDefinitions>
<Image HorizontalOptions="End"
VerticalOptions="Center"
Grid.Column="0"
Aspect="AspectFill">
<Image.Source>
<FontImageSource
FontFamily="{StaticResource FontAwesomeRegular}"
Glyph="{x:Static custom:FontAwesomeIcons.Envelope}"
Color="White"
Size="20" />
</Image.Source>
</Image>
<custom:CustomEntry BackgroundColor="Transparent"
TextColor="White"
PlaceholderColor="White"
VerticalOptions="Center"
HorizontalOptions="Fill"
Placeholder="Code"
Text="{Binding CompanyUser.CountryCode}"
Grid.Column="1" />
<custom:CustomEntry BackgroundColor="Transparent"
TextColor="White"
PlaceholderColor="White"
VerticalOptions="Center"
HorizontalOptions="Fill"
Placeholder="Phone"
Text="{Binding CompanyUser.Cellphone}"
Grid.Column="2" />
</Grid>
</pancakeView:PancakeView>
<Label Grid.Row="3"
Grid.Column="0"
Text="Cellphone"/>
<Button TextColor="White" Text="Sign Up" Command="{Binding SignUpCommand}" BackgroundColor="#93c393"
BorderColor="DarkSeaGreen" BorderWidth="1" Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2"/>
<Label Grid.Row="5"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalOptions="Center"
FontAttributes="Bold"
TextColor="White"
Text="{Binding AuthyUserResponse.Message}"/>
<Label Grid.Row="6"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalOptions="Center"
FontAttributes="Bold"
TextColor="White"
Text="{Binding AuthyOTPResponse.Message}"/>
<pancakeView:PancakeView Grid.Row="7" Grid.Column="0" Grid.ColumnSpan="2"
IsVisible="{Binding NeedsVerification}"
HeightRequest="40" BackgroundColor="Transparent" CornerRadius="5"
BorderThickness="1" BorderColor="White" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image HorizontalOptions="End"
VerticalOptions="Center"
Grid.Column="0"
Aspect="AspectFill">
<Image.Source>
<FontImageSource
FontFamily="{StaticResource FontAwesomeRegular}"
Glyph="{x:Static custom:FontAwesomeIcons.Envelope}"
Color="White"
Size="20" />
</Image.Source>
</Image>
<custom:CustomEntry BackgroundColor="Transparent"
TextColor="White"
PlaceholderColor="White"
VerticalOptions="Center"
HorizontalOptions="Fill"
Placeholder="PIN Code"
Text="{Binding Token}"
Grid.Column="1" />
</Grid>
</pancakeView:PancakeView>
<Button TextColor="White" Text="Verify" Command="{Binding VerifyTokenCommand}" BackgroundColor="#93c393"
IsVisible="{Binding NeedsVerification}"
BorderColor="DarkSeaGreen" BorderWidth="1" Grid.Row="8" Grid.Column="0" Grid.ColumnSpan="2"/>
<Label Grid.Row="9"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalOptions="Center"
IsVisible="{Binding NeedsVerification}"
FontAttributes="Bold"
TextColor="White"
Text="{Binding AuthyVerifyResponse.Message}"/>
</Grid>
</ContentPage.Content>
</ContentPage>
And let's see if it works!
First, fill-in the form with the name, email and phone. If the sign up button is clicked...
You'll actually receive a security code on your phone!
Type it correctly and you'll see the "Token is valid" message, which certifies that the user is who they claim to be.
Source code of the project is available here
As you can see, it is quite easy to start with 2FA in a C# app. Maybe you are concerned if it is a costly solution. You get 100 free authentications per month to test the service. Check the Authy pricing page for more details.
I also recommend you to have a look at the 2FA best practices page for advices and considerations when implementing 2FA in production.
I hope that this blog post was interesting and useful for you. I invite you to visit my blog for more technical posts about Xamarin, Azure, and the .NET ecosystem. 90% of the content is written in Spanish language, although if you don't speak/understand it (yet), no worries, I tend to write the code in English so there's still chance a visit might pay off =)
Thanks for your time, and enjoy the rest of the C# Advent Calendar publications!
See you next time,
Luis