This blog post details how to implement social authentication and provide users with several social login options and how we can handle the users' data obtained as a result of these logins in our application. In this blog post, we’ll look at how we can integrate Google and Facebook login authentications. We will see how this can be implemented from the server side of an application using .NET 6; Microsoft's own technology for building web applications. This is going to be built as a Web API Project.
If you want to follow along, create a .Net 6 project from visual studio.
Just to say a little about OpenID Connect and Oauth 2.0, OpenId Connect is an identity layer built on top of OAuth 2.0. It allows individuals to use single sign-on (SSO) to access relying party sites using OpenID Providers (OPs) such as Google, Facebook, Linkedin, GitHub, etc.
Google Authentication
We first need to create a Google developer account, if you already have an existing Gmail account, you can make use of it. To use Google Authentication we need to obtain an OAuth Client ID. To get that you need to create a new project, upon creating it, we can then have access to the ClientId and Client Secret.
"Google": {
"ClientId": "",
"ClientSecret": ""
},
From the block of code(s) below we need to register the services needed and then configure the google oauth client Id so we can use it any where needed in our application. We install a nugget package Google.Apis.Auth this provides us with the ability to communicate with google oauth server where validation is done on the client id against the generated id token from the client side. We can then have access to the authenticated user's data and persist it on our application database.
Create a class GoogleAuthConfig; this would help us bind the app settings value of the Google ClientId and Client Secret to the class.
public class GoogleAuthConfig
{
public string ClientId { get; set; }
public string ClientSecret { get; set; }
}
Register the following service in the program.cs
builder.Services.AddScoped<IGoogleAuthService, GoogleAuthService>();
builder.Services.Configure<GoogleAuthConfig>(builder.Configuration.GetSection("Google"));
Create another class GoogleSignInVM; this would represent our (Data Transfer Object - DTO) being exposed to the client side.
public class GoogleSignInVM
{
[Required]
public string IdToken { get; set; }
}
Create a new interface called IGoogleAuthService.
Note: I created a generic response class, BaseResponse, that I return as part of my response body. You can check mine from GitHub or use yours if available.
public interface IGoogleAuthService
{
Task<BaseResponse<User>> GoogleSignIn(GoogleSignInVM model);
}
Implement the interface IGoogleAuthService in the class GoogleAuthService and add the following code. I used Log4Net for logging in this project. Install the nugget package log4net.
public class GoogleAuthService : IGoogleAuthService
{
private readonly UserManager<User> _userManager;
private readonly ApplicationDbContext _context;
private readonly GoogleAuthConfig _googleAuthConfig;
private readonly ILog _logger;
public GoogleAuthService(
UserManager<User> userManager,
ApplicationDbContext context,
IOptions<GoogleAuthConfig> googleAuthConfig
)
{
_userManager = userManager;
_context = context;
_googleAuthConfig = googleAuthConfig.Value;
_logger = LogManager.GetLogger(typeof(GoogleAuthService));
}
public async Task<BaseResponse<User>> GoogleSignIn(GoogleSignInVM model)
{
Payload payload = new();
try
{
payload = await ValidateAsync(model.IdToken, new ValidationSettings
{
Audience = new[] { _googleAuthConfig.ClientId }
});
}
catch (Exception ex)
{
_logger.Error(ex.Message, ex);
return new BaseResponse<User>(null, new List<string> { "Failed to get a response." }); }
var userToBeCreated = new CreateUserFromSocialLogin
{
FirstName = payload.GivenName,
LastName = payload.FamilyName,
Email = payload.Email,
ProfilePicture = payload.Picture,
LoginProviderSubject = payload.Subject,
};
var user = await _userManager.CreateUserFromSocialLogin(_context, userToBeCreated, LoginProvider.Google);
if (user is not null)
return new BaseResponse<User>(user);
else
return new BaseResponse<User>(null, new List<string> { "Failed to get response." });
}
The Client side i.e. angular, flutter, react sends the id token generated from google authorization server to our endpoint and upon successful validation of the id token, a JWT is issued from our application so that the user can have access to other resource of the application.
Create a controller class AuthenticationController and add the following code.
[HttpPost]
[ProducesResponseType(typeof(BaseResponse<bool>), 200)]
public async Task<IActionResult> GoogleSignIn(GoogleSignInVM model)
{
try
{
return ReturnResponse(await _authService.SignInWithGoogle(model));
}
catch (Exception ex)
{
return HandleError(ex);
}
}
Facebook Authentication
As we did for Google, you guessed right, we first need to create a Facebook developer account. Where we would have access to create a new application so that we can obtain an AppId and App Secret.
Add the following code to the appsettings of your application together with the AppId and AppSecret.
"Facebook": {
"BaseUrl": "https://graph.facebook.com/",
"TokenValidationUrl": "debug_token?input_token={0}&access_token={1}|{2}",
"UserInfoUrl": "me?fields=id,name,email,first_name,last_name,picture&access_token={0}",
"AppId": "",
"AppSecret": ""
},
Add the following code also to the program.cs file of your application. We need to make an API request from our application to Facebook Graph API Explorer. Note: you only have access to this API after you have created your developer account. The Facebook graph API Explorer provides as with many functionalities(API’s) to work with, for this authentication purpose we need to validate the access token and get the authenticated user’s data. As you can see from the appsettings above that we added the API urls to make request to.
Create a class FacebookAuthConfig; this would help us bind the app settings value of the Facebook Configuration to the class.
public class FacebookAuthConfig
{
public string TokenValidationUrl { get; set; }
public string UserInfoUrl { get; set; }
public string AppId { get; set; }
public string AppSecret { get; set; }
}
Register the following service in the program.cs
builder.Services.AddScoped<IFacebookAuthService, FacebookAuthService>();
builder.Services.Configure<FacebookAuthConfig>(builder.Configuration.GetSection("Facebook"));
//Using Named Client
builder.Services.AddHttpClient("Facebook", c =>
{
c.BaseAddress = new Uri(builder.Configuration.GetValue<string>("Facebook:BaseUrl"));
});
When making an API Request to the Facebook Graph explorer, we get response data (Serialized data) from which we have to build an object to deserialize it into; so that we can have access to the information. The first method validates the access token issued by facebook coming from the client app i.e. angular, react, flutter, (when the users request to login), after successful validation, the second method gets the user information, with this we can have access to the authenticated user’s data and persist it on our application database.
Create an interface IFacebookAuthService.
public interface IFacebookAuthService
{
Task<BaseResponse<FacebookTokenValidationResponse>> ValidateFacebookToken(string accessToken);
Task<BaseResponse<FacebookUserInfoResponse>> GetFacebookUserInformation(string accessToken);
}
Implement the interface IFacebookAuthService in the class FacebookAuthService. Add the code below to the class.
public class FacebookAuthService : IFacebookAuthService
{
private readonly HttpClient _httpClient;
private readonly FacebookAuthConfig _facebookAuthConfig;
private readonly ILog _logger;
public FacebookAuthService(
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
IOptions<FacebookAuthConfig> facebookAuthConfig)
{
_httpClient = httpClientFactory.CreateClient("Facebook");
_facebookAuthConfig = facebookAuthConfig.Value;
_logger = LogManager.GetLogger(typeof(FacebookAuthService));
}
public async Task<BaseResponse<FacebookTokenValidationResponse>> ValidateFacebookToken(string accessToken)
{
try
{
string TokenValidationUrl = _facebookAuthConfig.TokenValidationUrl;
var url = string.Format(TokenValidationUrl, accessToken, _facebookAuthConfig.AppId, _facebookAuthConfig.AppSecret);
var response = await _httpClient.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var responseAsString = await response.Content.ReadAsStringAsync();
var tokenValidationResponse = JsonConvert.DeserializeObject<FacebookTokenValidationResponse>(responseAsString);
return new BaseResponse<FacebookTokenValidationResponse>(tokenValidationResponse);
}
}
catch (Exception ex)
{
_logger.Error(ex.StackTrace, ex);
}
return new BaseResponse<FacebookTokenValidationResponse>(null, "Failed to get response");
}
public async Task<BaseResponse<FacebookUserInfoResponse>> GetFacebookUserInformation(string accessToken)
{
try
{
string userInfoUrl = _facebookAuthConfig.UserInfoUrl;
string url = string.Format(userInfoUrl, accessToken);
var response = await _httpClient.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var responseAsString = await response.Content.ReadAsStringAsync();
var userInfoResponse = JsonConvert.DeserializeObject<FacebookUserInfoResponse>(responseAsString);
return new BaseResponse<FacebookUserInfoResponse>(userInfoResponse);
}
}
catch (Exception ex)
{
_logger.Error(ex.StackTrace, ex);
}
return new BaseResponse<FacebookUserInfoResponse>(null, "Failed to get response");
}
}
This is the JSON response gotten from the ValidateFacebookToken method when an API request was made to facebook graph to validate the access token that was generated from the client side. The response was then deserialized to the object below.
public class FacebookTokenValidationResponse
{
[JsonProperty("data")]
public FacebookTokenValidationData Data { get; set; }
}
public class FacebookTokenValidationData
{
[JsonProperty("app_id")]
public string AppId { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("application")]
public string Application { get; set; }
[JsonProperty("data_access_expires_at")]
public long DataAccessExpiresAt { get; set; }
[JsonProperty("expires_at")]
public long ExpiresAt { get; set; }
[JsonProperty("is_valid")]
public bool IsValid { get; set; }
[JsonProperty("metadata")]
public Metadata Metadata { get; set; }
[JsonProperty("scopes")]
public string[] Scopes { get; set; }
[JsonProperty("user_id")]
public string UserId { get; set; }
}
public class Metadata
{
[JsonProperty("auth_type")]
public string AuthType { get; set; }
}
This is the JSON response gotten from the *GetFacebookUserInformation * method when an API request was made to facebook graph to get the user’s information. The response was then deserialized to the object below.
public class FacebookUserInfoResponse
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("first_name")]
public string FirstName { get; set; }
[JsonProperty("last_name")]
public string LastName { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("picture")]
public Picture Picture { get; set; }
}
public class Picture
{
[JsonProperty("data")]
public Data Data { get; set; }
}
public class Data
{
[JsonProperty("height")]
public long Height { get; set; }
[JsonProperty("is_silhouette")]
public bool IsSilhouette { get; set; }
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("width")]
public long Width { get; set; }
}
Create a new class FacebookSignInVM; this would represent our (Data Transfer Object — DTO) being exposed to the client side.
The class below is an extension method that assists in creating users from social login. A check is done at first to see if the user hasn’t alredy been linked to the identity provider, which i called below as LoginProvider, if it has not we create a new user and add the user to the identity provider, else we return the user as it alredy exists.
public enum LoginProvider
{
Google = 1,
Facebook
}
public static class CreateUserFromSocialLoginExtension
{
public static async Task<User> CreateUserFromSocialLogin(this UserManager<User> userManager, ApplicationDbContext context, CreateUserFromSocialLogin model, LoginProvider loginProvider)
{
//CHECKS IF THE USER HAS NOT ALREADY BEEN LINKED TO AN IDENTITY PROVIDER
var user = await userManager.FindByLoginAsync(loginProvider.GetDisplayName(), model.LoginProviderSubject);
if (user is not null)
return user; //USER ALREADY EXISTS.
user = await userManager.FindByEmailAsync(model.Email);
if (user is null)
{
user = new User
{
FirstName = model.FirstName,
LastName = model.LastName,
Email = model.Email,
UserName = model.Email,
ProfilePicture = model.ProfilePicture
};
await userManager.CreateAsync(user);
//EMAIL IS CONFIRMED; IT IS COMING FROM AN IDENTITY PROVIDER
user.EmailConfirmed = true;
await userManager.UpdateAsync(user);
await context.SaveChangesAsync();
}
UserLoginInfo userLoginInfo = null;
switch (loginProvider)
{
case LoginProvider.Google:
{
userLoginInfo = new UserLoginInfo(loginProvider.GetDisplayName(), model.LoginProviderSubject, loginProvider.GetDisplayName().ToUpper());
}
break;
case LoginProvider.Facebook:
{
userLoginInfo = new UserLoginInfo(loginProvider.GetDisplayName(), model.LoginProviderSubject, loginProvider.GetDisplayName().ToUpper());
}
break;
default:
break;
}
//ADDS THE USER TO AN IDENTITY PROVIDER
var result = await userManager.AddLoginAsync(user, userLoginInfo);
if (result.Succeeded)
return user;
else
return null;
}
}
The interface and the class below is the AuthService that is exposed to the Authentication controller class. Upon Successful validation of id token (from google) or access token(from facebook) from the login providers, we then generate a Jwt token from our application, which you can see from the AuthService Class.
Create a class JwtResponseVM
public class JwtResponseVM
{
[Required]
public string Token { get; set; }
}
public interface IAuthService
{
Task<BaseResponse<JwtResponseVM>> SignInWithGoogle(GoogleSignInVM model);
Task<BaseResponse<JwtResponseVM>> SignInWithFacebook(FacebookSignInVM model);
}
Implement the interface IAuthService in the class AuthService. Add the code below to the class. you can notice that we injected the IGoogleAuthService and IFacebookAuthService interfaces.
public class AuthService : IAuthService
{
private readonly ApplicationDbContext _context;
private readonly IGoogleAuthService _googleAuthService;
private readonly IFacebookAuthService _facebookAuthService;
private readonly UserManager<User> _userManager;
private readonly Jwt _jwt;
public AuthService(
ApplicationDbContext context,
IGoogleAuthService googleAuthService,
IFacebookAuthService facebookAuthService,
UserManager<User> userManager,
IOptions<Jwt>jwt)
{
_context = context;
_googleAuthService = googleAuthService;
_facebookAuthService = facebookAuthService;
_userManager = userManager;
_jwt = jwt.Value;
}
public async Task<BaseResponse<JwtResponseVM>> SignInWithGoogle(GoogleSignInVM model)
{
var response = await _googleAuthService.GoogleSignIn(model);
if (response.Errors.Any())
return new BaseResponse<JwtResponseVM>(response.ResponseMessage, response.Errors);
var jwtResponse = CreateJwtToken(response.Data);
var data = new JwtResponseVM
{
Token = jwtResponse,
};
return new BaseResponse<JwtResponseVM>(data);
}
public async Task<BaseResponse<JwtResponseVM>> SignInWithFacebook(FacebookSignInVM model)
{
var validatedFbToken = await _facebookAuthService.ValidateFacebookToken(model.AccessToken);
if(validatedFbToken.Errors.Any())
return new BaseResponse<JwtResponseVM>(validatedFbToken.ResponseMessage, validatedFbToken.Errors);
var userInfo = await _facebookAuthService.GetFacebookUserInformation(model.AccessToken);
if (userInfo.Errors.Any())
return new BaseResponse<JwtResponseVM>(null, userInfo.Errors);
var userToBeCreated = new CreateUserFromSocialLogin
{
FirstName = userInfo.Data.FirstName,
LastName = userInfo.Data.LastName,
Email = userInfo.Data.Email,
ProfilePicture = userInfo.Data.Picture.Data.Url.AbsoluteUri,
LoginProviderSubject = userInfo.Data.Id,
};
var user = await _userManager.CreateUserFromSocialLogin(_context, userToBeCreated, LoginProvider.Facebook);
if (user is not null)
{
var jwtResponse = CreateJwtToken(user);
var data = new JwtResponseVM
{
Token = jwtResponse,
};
return new BaseResponse<JwtResponseVM>(data);
}
return new BaseResponse<JwtResponseVM>(null, userInfo.Errors);
}
private string CreateJwtToken(User user)
{
var key = Encoding.ASCII.GetBytes(_jwt.Secret);
var userClaims = BuildUserClaims(user);
var signKey = new SymmetricSecurityKey(key);
var jwtSecurityToken = new JwtSecurityToken(
issuer: _jwt.ValidIssuer,
notBefore: DateTime.UtcNow,
audience: _jwt.ValidAudience,
expires: DateTime.UtcNow.AddMinutes(Convert.ToInt32(_jwt.DurationInMinutes)),
claims: userClaims,
signingCredentials: new SigningCredentials(signKey, SecurityAlgorithms.HmacSha256));
return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}
private List<Claim> BuildUserClaims(User user)
{
var userClaims = new List<Claim>()
{
new Claim(JwtClaimTypes.Id, user.Id.ToString()),
new Claim(JwtClaimTypes.Email, user.Email),
new Claim(JwtClaimTypes.GivenName, user.FirstName),
new Claim(JwtClaimTypes.FamilyName, user.LastName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
return userClaims;
}
}
You also noticed from the code snippets that we have used a DbContext class, ApplicationDbContext, which was injected in the constructor, you can also create yours or check mine from the GitHub repository and we also have JWT implementation too, I added all of this to the project and they can be found from the GitHub repo too. This article mainly focuses on how you can implement social authentication in your projects.
Thanks for your time, I hope you find this article useful. You can access the source code of this post from GitHub.
You can also follow me on Linkedin
Top comments (0)