İlk olarak https://console.cloud.google.com/ adresine giderek yeni bir proje oluşturuyor ve ismine Custom Identity Project Auth ismini veriyoruz. Ardından menüde bulunan Library‘e tıklıyıp Google+ API‘yi etkinleştiriyoruz ve ilk açılan sayfaya tekrar dönüp yine menüden OAuth Consent Screen‘e giriş yapıyoruz. User Type‘ı External olarak belirleyip devam ediyoruz. Açılan sayfada App Name alanına Custom Identity Project değerini giriyor ve zorunlu olan email alanlarını da doldurduktan sonra devam ediyoruz. Açılan sayfada Add Or Remove Scopes butonuna tıklıyoruz. Ve filtre üzerinden aşağıdaki 3 aramayı yapıp bunları projeye dahil ediyor ve kayıt işlemini gerçekleştiriyoruz.
auth/userinfo.email
auth/userinfo.profile
openid
Ardından yine menüde bulunan Credentials‘a tıklıyoruz ve açılan sayfadan +Create Credantials > OAuth Client ID‘yi seçiyoruz ve Application Type’ı seçip devam ediyoruz. Açılan sayfada Name‘i Custom Identity Project Client olarak belirliyoruz. Authorized JavaScript Origins alanına uygulamayı localhost’ta çalıştırdığımız URL’i veriyoruz. Authorized redirect URIs alanına ise kullanıcı Google ile giriş işlemini gerçekleştirdikten sonra yönlendirileceği adresi tanımlayacağız. Bunun içi varsayılan olarak localhost adresimizin sonuna “/signin-google” ekleyerek ilgili alana giriyoruz, bu redirect url’sini biz kodun içerisinden değiştireceğiz, kaydet deyip işlemi tamamlıyoruz. Artık Client Id ve Client Secret değerlerine sahibiz.
Projeye sağ tıklayıp Manage User Secret deyip açılan secret.json dosyasını aşağıdaki şekilde düzenliyoruz.
{
"ExternalLoginProviders": {
"GoogleClientId": "<Google Client Id>"
"GoogleClientSecret": "<Google Client Secret>"
}
}
Bu aşamadan sonra ilk olarak Nuget üzerinden Microsoft.AspNetCore.Authentication.Google paketini projemize dahil ediyoruz. Program.cs dosyasına AddAuthentication servisini ekliyor ve aşağıdaki şekilde düzenliyoruz.
builder.Services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = builder.Configuration["ExternalLoginProviders:GoogleClientId"];
options.ClientSecret = builder.Configuration["ExternalLoginProviders:GoogleClientSecret"];
options.SignInScheme = IdentityConstants.ExternalScheme;
});
Ardından LoginViewModel.cs dosyasını aşağıdaki şekilde düzenliyoruz.
using Microsoft.AspNetCore.Authentication;
using System.ComponentModel.DataAnnotations;
namespace IdentityCustomProject.ViewModels
{
public class LoginViewModel
{
[Required]
public string UserName { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name ="Remember me")]
public bool RememberMe { get; set; }
public string ReturnUrl { get; set; } // Kullanıcının login işlemi öncesi bulunduğu url'i tutmak için kullanacağız.
public IList<AuthenticationScheme> ExternalLoginProviders { get; set; } // Google, Twitter vb. Authentication providerların listesini tutmak için kullanacağız.
}
}
AccountController.cs da bulunan HttpGet özelliğine sahip Login metodunu aşağıdaki şekilde düzenliyoruz.
[HttpGet]
public async Task<IActionResult> Login(string? returnUrl)
{
LoginViewModel loginViewModel = new LoginViewModel()
{
ReturnUrl = returnUrl, // Get metodunda url üzerindeki parametre otomatik olarak metodun parametresine binding edilecektir.
ExternalLoginProviders = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList() // External login providerların listesini elde edeceğiz ve bunu Login.cshtml üzerinde kullanabilir olacağız.
};
return View(loginViewModel);
}
Login.cshtml dosyamızı da aşağıdaki şekilde düzenliyoruz.
@model LoginViewModel
@{
ViewData["Title"] = "Login";
}
<div class=row>
<div class=col-md-6>
<div class="card mb-3">
<div class="card-body">
<h2>User Login with Email</h2>
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="mb-3">
<input asp-for="UserName" class="form-control" placeHolder="Username" />
<span asp-validation-for="UserName" class="text-danger"></span>
</div>
<div class="mb-3">
<input asp-for="Password" type="password" class="form-control" placeHolder="Password" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="mb-3">
<div class="checkbox">
<label asp-for="RememberMe" >
<input asp-for="RememberMe" />
@Html.DisplayNameFor(x => x.RememberMe)
</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
</div>
</div>
<div class=col-md-6>
<div class="card">
<div class="card-body">
<div class="d-grid gap-2">
@{
if(Model.ExternalLoginProviders.Count == 0) // External login providerlar ayarlanmadıysa kontrolü.
{
<span>There is not external login providers</span>
}
else
{
<form method="post" asp-action="ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl">
<div class="d-grid gap-2">
@foreach(var provider in @Model.ExternalLoginProviders) // Porgram.cs'detanımlanmış olduğumuz tüm login providerlar için buton oluşturuyoruz.
{
<button class="btn btn-primary" type="submit" asp-action="ExternalLogin"
name="provider" value="@provider.Name">Login with @provider.DisplayName</button>
}
</div>
</form>
}
}
</div>
</div>
</div>
</div>
</div>
İlgili provider’a tıklandığında çalışacak olan ExternalLogin metodunu aşağıdaki gibi yazıyoruz.
[HttpPost]
public async Task<IActionResult> ExternalLogin(string provider, string returnUrl)
{
var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new {ReturnUrl = returnUrl}); // Providerdan dönen datalar ExternalLoginCallback action'ında kontrol edilecek. Bu sebeple yönlendirme Url'i oluşturuyoruz.
var externalLoginProperties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); // Providerı ve yukarıda oluşturduğumuz redirectUrl'i parametre olarak veriyoruz.
return new ChallengeResult(provider, externalLoginProperties);
}
Şimdi sıra providerdan dönen datalarla authentication işleminin gerçekleşeceği metodu yazamaya geldi bunun için EternalLogin metodunda action Url oluştururken belirttiğimiz, ExternalLoginCallback metodunu oluşturup aşağıdaki şekilde düzenliyoruz.
[HttpGet]
public async Task<IActionResult> ExternalLoginCallback(string? returnUrl = null, string? remoteError = null)
{
returnUrl = returnUrl ?? Url.Content("~/"); // returnUrl null ise root url atamasını yapıyoruz.
LoginViewModel loginViewModel = new LoginViewModel()
{
ReturnUrl = returnUrl,
ExternalLoginProviders = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList()
};
if (remoteError != null) // Hata kontrolü yapıyoruz.
{
ModelState.AddModelError(string.Empty, $"Error from external login provider: {remoteError}");
return View("Login", loginViewModel);
}
var info = await _signInManager.GetExternalLoginInfoAsync(); // External login provider'a ait dataları alıyoruz.
if(info == null)
{
ModelState.AddModelError(string.Empty, $"Error loading external login informartion");
return View("Login", loginViewModel);
}
var signInReult = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); // Bu metod AspNetUserLogins tablosu üzerinden ilgili external login provider ile ilgili kullanıcı kontrolünü yapıyor.
if(signInReult.Succeeded) // Succeeded durumu kullanıcının bu daha öncesinde local account oluşturduğunu belirtir ve bu sebeple ilgili url'e yönlendirilme başarıyla işlemi gerçekleştilir.
{
return LocalRedirect(returnUrl);
}
else
{
var email = info.Principal.FindFirstValue(ClaimTypes.Email); // ClaimTypes atamaları providerdan gelen değerler ile otomatik olarak yapılış oluyor.
if (email != null)
{
// Bu aşamada kullanıcının providerdan gelen emaili üzerinden kullanıcı kontrolü tekrar yapılır.
// Çünkü kullanıcı daha önceden ilgili mail ile kayıt olmuş ancak google ile giriş yapmaya çalışıyor olabilir.
var user = await _userManager.FindByEmailAsync(email);
if(user == null)
{
// Kullanıcı bulunamadıysa oluşturduğumuz servis ile kullanıcı oluşturulur.
// Bu işlemi servis üzerinden gerçekleştirme sebebimiz oluşturduğumuz CreateUser metodunda
// unique bir Username oluşturmak için kullandığımız algoritmayı controllerdan ayırmaktır.
user = await _externalUserService.CreateUser(_userManager, info.Principal);
}
await _userManager.AddLoginAsync(user, info);
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
ViewBag.ErrorTitle = $"Email claim not received from {info.LoginProvider}";
ViewBag.ErrorMessage = "Please contact with Admin";
return View("Error");
}
return View("Login", loginViewModel);
}
Biz projemize başlarken kullanıcı adını kullanıcı belirleyebilir şeklinde tasarlamıştık. Fakat provider ile kayıt olan bir kullanıcının unique bir kullanıcı adına sahip olabilmesi için, kullanıcı veri tabanına kayıt edilirken kullanıcıya ait unique bir kullanıcı adı oluşturmamız ve ilgili kullanıcıya atamamız gerekmekte. Bu işlemi de yukarıdaki kodda belirtildiği gibi servis üzerinden gerçekleştirdik.
Servisimizi oluşturmak için projenin Services klasörü altına ExternalUserService altında bir klasör daha oluşturuyor ve bu klasörün içine de IExternalUserService adında bir servis ve ExternalUserService adında bir de class oluşturuyor ve aşağıdaki şekilde düzenliyoruz.
using IdentityCustomProject.Models;
using Microsoft.AspNetCore.Identity;
using System.Security.Claims;
namespace IdentityCustomProject.Services.ExternalUserService
{
public interface IExternalUserService
{
public Task<AppUser> CreateUser(UserManager<AppUser> userManager, ClaimsPrincipal principal);
}
}
using IdentityCustomProject.Data;
using IdentityCustomProject.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using System.Text.RegularExpressions;
namespace IdentityCustomProject.Services.ExternalUserService
{
public class ExternalUserService : IExternalUserService
{
private readonly ApplicationDbContext _dbContext;
private readonly UserManager<AppUser> _userManager;
public ExternalUserService(ApplicationDbContext dbContext, UserManager<AppUser> userManager)
{
_dbContext = dbContext;
_userManager = userManager;
}
public async Task<AppUser> CreateUser(UserManager<AppUser> userManager, ClaimsPrincipal principal)
{
var userName = principal.FindFirstValue(ClaimTypes.Name);
userName = userName.Replace(" ", "");
userName = Regex.Replace(userName, @"[^0-9a-zA-Z._]+", "");
userName = userName.ToLower();
var user = await _dbContext.Users.FirstOrDefaultAsync(x => x.UserName == userName);
if (user != null)
{
user = new AppUser
{
Name = principal.FindFirstValue(ClaimTypes.Name),
Surname = principal.FindFirstValue(ClaimTypes.Surname),
UserName = "?",
Email = principal.FindFirstValue(ClaimTypes.Email)
};
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
await _userManager.SetUserNameAsync(user, userName + user.Id);
}
}
return user;
}
}
}
ExternalUserService sınıfımızda bulunan CreateUser metodumuza parametre olarak, UserManager<AppUser> ve providerdan gelen ClaimsPrincipal’ı veriyor ve user oluşturup bu user’ı dönüyoruz.