Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

Asp.NET Core 5.0 – Angular’da JWT Eşliğinde Microsoft&Google Authenticator İle Two Factor Authentication

Merhaba,

Hatırlarsanız eğer Two Factor Authentication Nedir? başlıklı makalemizde Two Factor Authentication’ın ne olduğundan bahsetmiştik ve Google & Microsoft Authenticator İle Two Factor Authentication başlıklı makalemizde ise Asp.NET Core MVC mimarisine özel çift aşamalı doğrulamanın nasıl gerçekleştirildiğini ele almıştık. Bu içeriğimizde ise frontend’de bir SPA mimarisi olan Angular ile backend’de Asp.NET Core API’ları tüketilirken bir yandan da Two Factor Authentication işleminin nasıl gerçekleştirildiğini ele alıyor olacağız.

Başlarken

İçeriğimizin seyrine başlamadan önce üzerinde geliştirme yapabilmek için elinizde hali hazırda Angular ve Asp.NET Core Web API uygulamalarının olması gerekmektedir. Bunun için sizlerin konuyu rahat takip edebilmesi ve herhangi bir karışıklılığa mahal verilmemesi için JWT konfigürasyonuyla birlikte belli başlı aksiyonları önceden hazırlayarak oluşturduğum Asp.NET Core 5.0 Web API ve Angular projelerini aşağıdan indirip ele alınacak olan konuya odaklı bir şekilde çalışmanızı gerçekleştirebilirsiniz.

Sizlere önceden hazırlık maksadıyla sunulan bu projelerden Asp.NET Core uygulamasında, kullanıcı kaydıyla birlikte JWT tabanlı kullanıcı girişi desteği sağlanmaktadır. Angular uygulamasında ise gerekli işlemlere eşlik edebilen component’lar tasarlanmış ve guard yapısı ile API’dan gelen JWT’ye göre UI tabanlı authorize işlemleri sağlanmıştır. Tek yapılması gereken two factor authentication desteğinin sisteme entegre edilmesidir. Bunuda bu içeriğimizde birlikte gerçekleştiriyor olacağız…

TwoFactorAuthenticationBackendExample

TwoFactoryAuthenticationUIExample

İlgili projeleri indirdiğinizde Asp.NET Core uygulamasının ‘Startup.cs’ dosyasına göz atarsanız eğer;
Asp.NET Core 5.0 - Angular'da JWT İle Birlikte Two Factor AuthenticationGörüldüğü üzere gerekli context konfigürasyonu eşliğinde identity mekanizması entegre edilmiştir. Identity servisini gördüğünüzde Hocam, hani JWT kullanacaktık? diye aklınızdan geçebilir. Lakin biz two factor authentication yapacağımızdan dolayı burada identity mekanizmasının nimetlerinden faydalanacağız. O yüzden altyapı olarak identity mekanizması kullanılmakta ve özellikle kullanıcı kayıt ve giriş işlemleri için tercih edilmekte lakin kullanıcı yetkilendirme için ise JWT kullanılmaktadır. Nihayetinde ilgili dosyada JWT konfigürasyonununda yapıldığı gözlemlenebilmektedir. Yani uzun lafın kısası, JWT ile birlikte two factor authentication işlemi yapabilmeniz için identity mekanizmasındaki doğrulayıcı metotlara ihtiyaç olacağından dolayı ilgili mimariyle işlem yapılması gerekmektedir.

Burada dikkat edilmesi gereken bir nokta mevcuttur. O da görselde vurgulandığı gibi ‘AddAuthentication’ servisinde belirtilen şema bildirileridir. Eğer ki, bir uygulamada JWT ve identity mekanizmalarını bu şekilde bütünleşik olarak kullanacaksanız kesinlikle ilgili şemaların belirtilmesi zaruridir. Aksi taktirde süreçte yetkilendirmeye dair bazı olası hatalar söz konusu olabilmektedir.

Asp.NET Core 5.0 - Angular'da JWT İle Birlikte Two Factor AuthenticationAyriyeten yine aynı uygulama içerisinde kullanıcı kayıt ve giriş işlemlerinden sorumlu olan ‘AuthController’ isimli controller sınıfındaki ‘SignIn’ fonksiyonuna göz atarsanız eğer şifreyi doğrulamak ve girişi onaylamak için identity mekanizmasındaki ‘SignInManager’ class’ı kullanılmaktadır. Bu onaydan sonra client’ı yetkilendirmek için ise bir JWT üretilmektedir.

Two Factor Authentication İnşası

Evet, artık ilgili projelerde two factor authentication inşasına başlayabilliriz. Bunun için aşağıdaki önergeleri adım adım takip edebilirsiniz;
Angular Uygulamasında Profil Sayfasının Düzenlenmesi
İlk olarak Angular uygulamasında profil sayfasının, kullanıcının iki aşamalı doğrulama seçeneğini seçebileceği şekilde düzenlenmesi gerekmektedir.
profile.component.html :

<div class="mb-3 form-check">
   <input type="checkbox" class="form-check-input" [(ngModel)]="cbTwoFactorAuth">
   <label class="form-check-label">Two Factor Authentication</label>
</div>
<div class="mb-3" *ngIf="cbTwoFactorAuth">
   <label class="form-check-label">Çift aşamalı doğrulama yöntemini seçiniz.</label><br>
   <select #selectAuth class="form-select" (change)="changeAuthMethod()" [(ngModel)]="selectAuthMethod">
      <option value="authenticator">Authenticator</option>
   </select>
</div>
 
<div *ngIf="selectAuthMethod == 'authenticator' && verificationCode && cbTwoFactorAuth">
   <div>
      <qr-code [value]="verificationCode.qrCodeUri" [size]="150"></qr-code>
   </div>
   <div class="mb-3">
      <label class="form-label">Doğrulama kodu</label>
      <input type="text" class="form-control" placeholder="Doğrulama kodu" id="verificationCode" >
   </div>
</div>
 
<button type="submit" class="btn btn-primary" (click)="Dogrula()">Kaydet</button>

profile.component.ts :

import { HttpHeaders } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { VerificationCode } from 'src/app/entities/verificationCode';
import { HttpService } from 'src/app/services/http.service';

import * as $ from "jquery";
@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {

  constructor(private httpService: HttpService) { }

  cbTwoFactorAuth: boolean = false;
  selectAuthMethod: any;
  verificationCode: VerificationCode;

  ngOnInit(): void { }

  changeAuthMethod() {
    if (this.selectAuthMethod == "authenticator")
      this.httpService.get("https://localhost:5001/api/auth/getauthenticatorverifycode", new HttpHeaders({ "Authorization": `Bearer ${localStorage.getItem("access_token")}` })).subscribe((codes: VerificationCode) => {
        this.verificationCode = codes;
      });
  }

  Dogrula() {
    this.httpService.get(`https://localhost:5001/api/auth/twofactorenable/${this.cbTwoFactorAuth ? 1 : 0}/${$("#verificationCode").val()}`, new HttpHeaders({ "Authorization": `Bearer ${localStorage.getItem("access_token")}` }))
      .subscribe((data: any) => {
        alert(data.message);
      });
  }
}

Yukarıdaki profile.component.ts dosyasına göz atarsanız eğer select listesinden seçilen yöntem eğer ‘authenticator’ değerine sahipse api’da ki auth/getauthenticatorverifycode adresine bir istek göndermekte ve böylece authenticator doğrulama kodunu talep etmektedir.

Backend’de ilgili kodu üretecek servisin içeriği ise aşağıdaki gibi olacaktır.

    public class AuthenticatorService
    {
        readonly UserManager<AppUser> _userManager;
        readonly UrlEncoder _urlEncoder;
        public AuthenticatorService(UserManager<AppUser> userManager, UrlEncoder urlEncoder)
        {
            _userManager = userManager;
            _urlEncoder = urlEncoder;
        }
        public async Task<string> GenerateSharedKey(AppUser user)
        {
            string sharedKey = await _userManager.GetAuthenticatorKeyAsync(user);
            if (string.IsNullOrEmpty(sharedKey))
            {
                IdentityResult result = await _userManager.ResetAuthenticatorKeyAsync(user);
                if (result.Succeeded)
                    sharedKey = await _userManager.GetAuthenticatorKeyAsync(user);
            }
            return sharedKey;
        }
        public async Task<string> GenerateQrCodeUri(string sharedKey, string title, AppUser user) =>
            $"otpauth://totp/{_urlEncoder.Encode(title)}:{_urlEncoder.Encode(user.Email)}?secret={sharedKey}&issuer={_urlEncoder.Encode(title)}";
    }

Ve bu servisi kullanarak doğrulama kodunun üretim talebini karşılayacak olan ‘AuthController’ içerisindeki ‘GetAuthenticatorVerifyCode’ isimli actionın içeriği ise;

        [HttpGet("[action]")]
        [Authorize]
        public async Task<IActionResult> GetAuthenticatorVerifyCode()
        {
            AppUser user = await _userManager.FindByNameAsync(User.Identity.Name);
            string sharedKey = await _authenticatorService.GenerateSharedKey(user);
            string qrcodeUri = await _authenticatorService.GenerateQrCodeUri(sharedKey, "Angular Two Factor Authentication Example", user);
            return Ok(new
            {
                sharedKey = sharedKey,
                qrCodeUri = qrcodeUri
            });
        }

Asp.NET Core 5.0 - Angular'da JWT Eşliğinde Microsoft&Google Authenticator İle Two Factor Authenticationşeklinde olacaktır. Tabi burada ilgili action ‘Authorize’ olduğu için token ile tetiklenmektedir ve içerikteki koda bakarsanız User.Identity.Name komutu ile o anki yetkili kullanıcının adına erişilmeye çalışılmaktadır. Burada ilgili datanın gelebilmesi için ‘Startup.cs’ dosyasında JWT servisinin eklendiği ‘AddJwtBearer’ metodunda aşağıdaki gibi ‘NameClaimType’ değerinin verilmesi gerekmektedir. Bu özellik, istekle birlikte gelen JWT’de ki ‘name’ isimli claim’i almakta ve User.Identity.Name kodunda kullanmaktadır. Bu bilginin ardından aklınıza Peki kullanıcıya özel üretilen JWT’ye ‘name’ claim’i nerede eklenmektedir? gibisinden haklı bir soru doğabilir.

Asp.NET Core 5.0 - Angular'da JWT Eşliğinde Microsoft&Google Authenticator İle Two Factor AuthenticationBu soru cevabını esasında kendi içerisinde saklamaktadır. Yani JWT üretilirken eklenmektedir. Yandaki görseli incelerseniz eğer JWT üretiminden sorumlu olan ‘TokenHandlerService’ isimli servis üretilen token’a ‘name’ claim’ini eklenmekte ve o şekilde kullanıcıya sunmaktadır. Konuyu daha iyi anlayabilmek için size önceden sunmuş olduğum proje içerisinde gerekli gözlemi yapabilir ve direkt olayı birincil gözden kavrayabilirsiniz.

Asp.NET Core 5.0 - Angular'da JWT Eşliğinde Microsoft&Google Authenticator İle Two Factor AuthenticationVelhasıl, yapılan bu çalışma neticesinde kullanıcı profil sayfasına gelerek, two factor authentication talebinde bulunur ve yöntem olarak ‘Authenticator’ı seçerse(ki örneğimizde başka yöntem bulunmamaktadır) yandaki gibi bir qrcode oluşturulacak ve kullanıcıdan doğrulama beklenecektir. Doğrulama işlemi için kaynağı aşağıdaki gibi olan auth/twofactorenable adresi tetiklenmektedir.

        [HttpGet("[action]/{state}/{verificationCode}")]
        [Authorize]
        public async Task<object> TwoFactorEnable(int state, string verificationCode)
        {
            bool result = false;
            AppUser user = await _userManager.FindByNameAsync(User.Identity.Name);
            if (state == 1)
                result = await _userManager.VerifyTwoFactorTokenAsync(user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);

            if (!result && state == 1)
                return Ok(new
                {
                    State = false,
                    Message = "Doğrulama kodu hatalı."
                });

            user.TwoFactorEnabled = (state == 1 ? true : false) && result;
            await _userManager.UpdateAsync(user);
            await _userManager.UpdateSecurityStampAsync(user);

            return Ok(new
            {
                State = user.TwoFactorEnabled,
                Message = $"Çift aşamalı doğrulama başarıyla {(user.TwoFactorEnabled ? "aktifleştirildi." : "pasifleştirildi.")}"
            });
        }

Burada ‘VerifyTwoFactorTokenAsync’ metoduyla bir çift aşamalı doğrulama token’ı üretileceğinden dolayı ‘No IUserTwoFactorTokenProvider named ‘Authenticator’ is registered.‘ hatasıyla karşılaşmamak için ‘AddDefaultTokenProviders’ servisini uygulamaya dahil etmeyi unutmayınız. Ayrıca yukarıdaki kodu incelerseniz eğer girilen kodun doğru olup olmadığı denetlenmekte ve ‘state’ değerine göre two factor authentication’ı aktif yahut pasif olarak düzenlemektedir.
Asp.NET Core 5.0 - Angular'da JWT Eşliğinde Microsoft&Google Authenticator İle Two Factor Authentication
Kullanıcı two factor authentication özelliğini aktifleştirdikten sonra kullanıcı bilgileriyle tekrar oturum açmak istediği zaman yukarılarda ele alındığı gibi SignInManager sınıfının ‘PasswordSignInAsync’ metoduyla doğrulama yapılacak lakin ‘TwoFactorEnabled’ kolonu true/1 olduğu taktirde ikinci aşamalı doğrulamayı gerektireceğinden dolayı girişe izin vermeyecektir. Haliyle bu durumda client’ı ikinci doğrulama aşamasının gereksinimine dair bilgilendirmek gerekecektir. Dolayısıyla bu işlevsellik için backend’e gidip ‘SignIn’ metodunu aşağıdaki gibi güncellememiz gerekecektir.
Asp.NET Core 5.0 - Angular'da JWT Eşliğinde Microsoft&Google Authenticator İle Two Factor AuthenticationBu değişiklik orjinali üzerinde çok spesifik olduğu için hızlıca yakalayabilmeniz için kod yerine görsel olarak vurgulamayı tercih ettim. Görüldüğü üzere ‘PasswordSignInAsync’ kontrolü neticesinde bir başarısızlık varsa ve bu two factor authentication’dan kaynaklıysa client’a buna dair bir bilgilendirmede bulunmaktayız. Dolayısıyla client edineceği bu bilgi sayesinde kullanıcıyı oturum açma esnasında ikinci adıma yönlendirecektir.

Angular uygulamasında bu duruma istinaden gerekli aksiyon aşağıdaki gibi alınabilir;
signin.component.ts :

import { Component, OnInit } from '@angular/core';
import { UserCreate } from 'src/app/entities/userCreate';
import { UserLogin } from 'src/app/entities/userLogin';
import { HttpService } from 'src/app/services/http.service';

import * as $ from "jquery";
import { Router } from '@angular/router';
import { UserNameService } from 'src/app/services/user-name.service';

@Component({
  selector: 'app-signin',
  templateUrl: './signin.component.html',
  styleUrls: ['./signin.component.css']
})
export class SigninComponent implements OnInit {

  constructor(private httpService: HttpService, private router: Router, private userNameService: UserNameService) { }

  ngOnInit(): void {
  }

  signIn(userNameorEmail: HTMLInputElement, password: HTMLInputElement) {
    const user: UserLogin = new UserLogin(userNameorEmail.value, password.value);
    this.httpService.post("https://localhost:5001/api/auth/signin", user).subscribe((data: any) => {
      const message = $("#message");
      message.addClass("alert-" + (data.signInState == true ? "success" : "danger"));
      message.html(data.signInState == true ? "Giriş başarılı." : "Kullanıcı adı veya şifre hatalı. Lütfen tekrar deneyiniz.");
      if (data.signInState) {
        localStorage.setItem("access_token", data.token.accessToken);
        localStorage.setItem("refresh_token", data.token.refreshToken);

      } else if (data.requiresTwoFactor) {
        //Two Factor Authentication için ikinci aşamaya/sayfaya yönlendirme yapılıyor...
        this.userNameService.UserName = user.userNameOrEmail;
        return this.router.navigateByUrl("sign-in-two-factor");
      }

      message.fadeIn(2000, () => {
        setTimeout(() => {
          message.fadeOut(2000, () => message.removeClass("alert-" + (data.signInState == true ? "success" : "danger")));
        }, 2000);
      });
    });
  }
}

signin-two-factor.component.ts :

import { HttpHeaders } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { HttpService } from 'src/app/services/http.service';
import { UserNameService } from 'src/app/services/user-name.service';

@Component({
  selector: 'app-signin-two-factor',
  templateUrl: './signin-two-factor.component.html',
  styleUrls: ['./signin-two-factor.component.css']
})
export class SigninTwoFactorComponent implements OnInit {

  constructor(private httpService: HttpService, private userNameService: UserNameService) { }

  ngOnInit(): void {
  }
  verify(verificationCode: string) {
    this.httpService.post("https://localhost:5001/api/auth/twofactorauthenticate", {
      VerificationCode: verificationCode.toString(),
      UsernameOrEmail: this.userNameService.UserName
    })
      .subscribe((data: any) => {
        if (data.accessToken != null) {
          localStorage.setItem("access_token", data.accessToken);
          localStorage.setItem("refresh_token", data.refreshToken);
        } else
          alert("Doğrulama kodu hatalı...");
      });
  }
}

İşte bu kadar 🙂 Artık uygulamaları derleyip, çalıştırabilir ve test edebiliriz.
Asp.NET Core 5.0 - Angular'da JWT Eşliğinde Microsoft&Google Authenticator İle Two Factor Authentication
Görüldüğü üzere Angular’da Microsoft&Google authenticator ile two factor authentication’ı başarılı bir şekilde uygulamış olduk.

Benim için sanırım en zor ve anlatımı en kompleks içeriklerden biri oldu diyebilirim 🙂 Sabredip okuduğunuz için teşekkür ederim…

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not : Örnek uygulamaların son halini indirebilmek için aşağıdaki adreslere tıklayınız.
Asp.NET Core 5.0 - Angular'da JWT Eşliğinde Microsoft&Google Authenticator İle Two Factor Authentication

TwoFactorAuthenticationBackendExample

TwoFactoryAuthenticationUIExample

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

*