IdentityServer Implementation
ASP.NET Core tabanlı IdentityServer, OAuth 2.0 ve OpenID Connect (OIDC) protokollerini uygulayan yetkilendirme sunucusudur. API, Web, Mobil, SPA gibi farklı platform ve uygulamalara yönelik ortak bir yetkilendirme yöntemi sunmaktadır. IdentityServer kütüphanesi open-source ve ihtiyaçlar doğrultusunda özelleştirmelere açıktır. Literatürde OpenID Connect Provider, Authorization Server, Identity Provider ve STS (Security Token Service) isimlerinde geçmektedir.
Günümüz modern uygulamalarında bir Client (web, mobil) çoğu zaman birden fazla API ile haberleşmektedir. API uygulamaları çeşitli API kaynaklarını da arkaplanda tüketebilmektedir. Birbirinden bağımsız, dağıtık bu uygulamala ve kaynakların dış dünyaya kapatılarak güven altına alınması gerekmektedir. Yetkilendirme işlemlerinin uygulama bazında implemente edilmesi kod tekrarı, zaman kaybı ve yönetim zorluklarına neden olmaktadır. Bu sorunların üstesinden gelmek için authentication ve authorization işlemlerinin merkezileştirilmesi gerekmektedir.
Benzer şekilde bir firmanın aynı kullanıcı kitlesine sahip dış dünyaya açılan farklı uygulamaları olabilmektedir. Yetkilendirme yapısının merkezileştirilmediği senaryoda kullanıcıların bu uygulamalara tek tek giriş ve çıkış yapmaları gerekecektir. IdentityServer uygulamalar arasında kullacılara single sign-in ve single sign-out özelliği de sunmaktadır.
Yazı boyunca ele alınan başlıklar aşağıdaki şekildedir.
- Client Credentials Flow
- Hybrid Flow
- Implicit Flow
- Authorization Code with PKCE
- Resource Owner Password Flow
- IdentityServer Custom UserStore
- IdentityServer ASP.NET Identity
- IdentityServer Entity Framework Core
IdentityServer OAuth 2.0 ve OpenID Connect protokollerini uyguladığından bu protokoller hakkında bilgi sahibi olunması gerekmektedir. Aksi taktirde aşağıdaki akışlar anlaşılmayacaktır.
OAuth 2.0 protokolünde iletişim halindeki tarafları tanımlayan bir takım roller mevcuttur.
- Resource owner korunan veri ve hizmete erişim izni veren varlıktır.
- Resource server korunan veri ve hizmetleri barındıran sunucudur.
- Client resource owner adına korunan veri ve hizmetlere istekte bulunan uygulamadır.
- Authorization server resource owner kimliğini doğrulayıp client uygulamalarını yetkilendirerek access token dağıtır.
IdentityServer kütüphanesi implemente edilirken bu tarafların authorization server rolünü üstlenen projeye tanıtılması gerekmektedir.
Client Credentials Flow
API kaynaklarının dış dünyaya kapatılarak yalnızca belirli client uygulamalarıyla iletişim kurması istendiği durumlarda kullanılması gereken akıştır.
Yetkilendirmeye tabi olmayan client uygulamaları koruma altına alınan API kaynaklarını tüketmek istemektedir. Client uygulaması öncelikle kendisini authorization server‘a client olarak kaydetmesi gerekmektedir. Authorization server kayıt işlemiyle client uygulamasına client credential (client id
, client secret
) bilgilerini verir. Sonrasındaysa client client id
ve client secret
bilgileriyle authorization server‘a giderek bir access token elde eder. (1) Client uygulaması elde ettiği access token ile resource server kaynaklarını tüketir. (2)
Authorization Server
Öncelikle authorization server rolünü üstlenen projeyi oluşturup gerekli IdentityServer4 NuGet paketini kuralım.
dotnet new web -n AuthServer
dotnet add package IdentityServer4
Bu akışta kullanacağımız OAuth 2.0 protokolünde rollerimizi temsil edecek kaynaklarımızı tanımlayalım.
public class Config
{
public static IEnumerable<ApiResource> ApiResources => new List<ApiResource>
{
new()
{
Name = "api",
DisplayName = "Api #1",
ApiSecrets = { new("api.secret".Sha256()) },
Scopes = { "api.read", "api.upsert", "api.delete" }
}
};
public static IEnumerable<ApiScope> ApiScopes => new List<ApiScope>
{
new("api.read", "Read permission"),
new("api.upsert", "Upsert permission"),
new("api.delete", "Delete permission")
};
public static IEnumerable<Client> Clients => new List<Client>
{
new()
{
ClientId = "app",
ClientSecrets = { new("app.secret".Sha256()) },
ClientName = "Client #1 Application",
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = { "api.read" }
}
};
}
API kaynaklarını ApiResource
client uygulamalarını Client
ve uygulama içerisinde kullanılacak kapsamlar ApiScope
ile tanımlanmaktadır. Scope olarak Client
modeline doğrudan ApiResource
Name
değeri verilebilir. Client
modelinde ClientId
ve ClientSecret
bilgileri tanımlanmıştır.
Gerekli servis ve middleware konfigürasyonlarını yapalım.
builder.Services.AddIdentityServer()
.AddInMemoryApiResources(Config.ApiResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddDeveloperSigningCredential();
app.UseIdentityServer();
IdentityServer konfigürasyonları yapılarak kaynaklar in-memory olarak eklenmiştir. Development ortamında kullanılacak bir sertifika olmadığı durumda AddDeveloperSigningCredential
methodu kullanılmalıdır. Uygulama ilk ayağa kalktığında tempkey.jwk
isminde geçici bir imzalanmış anahtar oluşturulmaktadır. Production ortamında AddSigningCredential
methodu kullanılmalıdır.
Client uygulaması credential bilgileriyle bir endpoint üzerinden access token almaktadır. IdentityServer kullanıma hazır bir grup endpoint tanımlar.
- Discovery endpoint IdentityServer hakkında metadata bilgisine ulaşmak için kullanılmaktadır. Issuer, diğer endpoint adresleri, desteklenen kapsamlar gibi bilgileri barındırır.
/.well-known/openid-configuration
adresinden ulaşılır. - Authorize endpoint User-Agent üzerinden token ve authorization code almak için kullanılır.
/connect/authorize
adresine sahiptir. - Token endpoint token elde etmek için kullanılır.
/connect/token
adresine sahiptir. - UserInfo endpoint yetkilendirilen kullanıcı hakkındaki bilgileri elde etmek için kullanılmaktadır.
/connect/userinfo
adresine sahiptir. - Device authorization endpoint device flow authorization işlemlerini başlatmak için kullanılır.
/connect/deviceauthorization
adresine sahiptir. - Introspection endpoint token doğrulamak için kullanılır.
/connect/introspect
adresine sahiptir. - Revocation endpoint access token ve refresh token bilgilerini iptal ederek geçersiz kılar.
/connect/revocation
adresine sahiptir. - End session endpoint single sign-out işlemini tetiklemek için kullanılır.
/connect/endsession
adresine sahiptir.
Resource Server
Resource server olarak bir API projesi oluşturalım. Bir token geldiğinde doğrulanabilmesi ve JWT Authentication işlemleri için Microsoft.AspNetCore.Authentication.JwtBearer
NuGet paketini kuralım.
dotnet new webapi -n Api
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Yetkilendirme servislerini DI olarak ekleyip middleware ayarlarını yapalım.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = "https://localhost:7000";
options.Audience = "api";
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Read", policy =>
{
policy.RequireClaim("scope", "api.read");
});
options.AddPolicy("Upsert", policy =>
{
policy.RequireClaim("scope", "api.upsert");
});
options.AddPolicy("Delete", policy =>
{
policy.RequireClaim("scope", "api.delete");
});
});
app.UseAuthentication();
app.UseAuthorization();
Kimlik doğrulamayla ilgili servisler AddAuthentication
methodu ve Bearer
varsayılan scheme kullanılarak ekleniyor. Yetkilendirmeyle ilgili servisler AddAuthorization
methoduyla eklenerek kapsamlar doğrultusunda Claim-Based Authorization gerçekleştirmek üzere AddPolicy
methoduyla eklenmektedir.
Controller sınıfımızı oluşturup dummy data kullanarak CRUD işlemleri gerçekleştirelim.
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private static List<Product> Items => new List<Product>
{
new() { Id = 1, CategoryId = 1, Name = "Pencil", Price = 12.25m },
new() { Id = 2, CategoryId = 2, Name = "Book", Price = 19.50m },
new() { Id = 3, CategoryId = 1, Name = "Eraser", Price = 5.0m },
};
[Authorize(Policy = "Read")]
public IActionResult Get() => Ok(Items);
[HttpPost]
[Authorize(Policy = "Upsert")]
public IActionResult Post(Product product)
{
Items.Add(product);
return Ok();
}
[HttpPut]
[Authorize(Policy = "Upsert")]
public IActionResult Put(int id, Product product)
{
var item = Items.FirstOrDefault(f => f.Id == id);
if (item != null) item = product;
return Ok();
}
[HttpDelete]
[Authorize(Policy = "Delete")]
public IActionResult Delete(int id)
{
var item = Items.FirstOrDefault(f => f.Id == id);
if (item != null) Items.Remove(item);
return Ok();
}
}
Görüldüğü üzere action methodlar Authorize
attribute ile işaretlenerek oluşturulan Policy‘ler içerisinde tanımlanmıştır.
Client Application
Client uygulamasını temsil edecek MVC projesini oluşturalım ve IdentityModel
NuGet paketini kuralım. IdentityModel paketi OpenID Connect ve OAuth 2.0 protokolü için HttpClient
sınıfını genişleten bir dizi extension method tanımlar. Authorization Server üzerinden token alınırken ve Resource Server kaynakları tüketilirken bu methodlardan faydalanılacaktır.
dotnet new mvc Client
dotnet add package IdentityModel
Controller sınıfımızı valid bir token edinip bu token ile resource server kaynaklarını listeleyecek şekilde kodlayalım.
public class ProductsController : Controller
{
public async Task<IActionResult> Index()
{
var httpClient = new HttpClient();
var discoveryDocument = await httpClient.GetDiscoveryDocumentAsync("https://localhost:7000");
if (discoveryDocument.IsError) { /* Handle error */ }
var tokenRequest = new ClientCredentialsTokenRequest
{
ClientId = "app",
ClientSecret = "app.secret",
Address = discoveryDocument.TokenEndpoint
};
var tokenResponse = await httpClient.RequestClientCredentialsTokenAsync(tokenRequest);
if (tokenResponse.IsError) { /* Handle error */ }
httpClient.SetBearerToken(tokenResponse.AccessToken);
var response = await httpClient.GetAsync("https://localhost:7001/api/items");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var products = JsonSerializer.Deserialize<List<Product>>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
return View(products ?? new());
}
return View();
}
}
IdentityServer metadata bilgisi GetDiscoveryDocumentAsync
methoduyla elde ediliyor. Sonrasındaysa bu metadata bilgisi içerisindeki TokenEndpoint
adresi ve client credential
bilgileri kullanılarak RequestClientCredentialsTokenAsync
methoduyla token elde ediliyor. İşlem başarılıysa resource server‘a istek atılmadan önce Authorization header SetBearerToken
methoduyla ayarlanıyor.
Client uygulaması resource server kaynaklarını kapsamları (scope
) doğrultusunda tüketmeye hazır.
Hybrid Flow
Bu akış Implicit ve Authorization Code akışlarının birleşimidir. Akışlar, Response Type parametresinde belirtilen değere göre şekillenir.
Flow | Response Type |
---|---|
Authorization Code | code |
Implicit | id_token |
Implicit | id_token token |
Hybrid | code id_token |
Hybrid | code token |
Hybrid | code id_token token |
Implicit akışta tüm token bilgileri front-channel üzerinden gönderilmektedir. Identity token için bu sorun olmasada gerekli olmadığı durumlarda access token dış dünyaya açılmamalıdır. Client uygulaması öncelikle front-channel üzerinden aldığı identity token ile doğrulama işlemi gerçekleştirir. Sonrasındaysa client uygulaması back-channel üzerinden access token elde eder.
Client uygulaması resource owner adına resource server bünyesindeki bilgilere erişmek istemektedir. Client uygulaması resource owner‘ı gerekli bilgileri query string parametrelerine ekleyerek authorization server‘a yönlendirir. (1) Yönlendirme esnasında scope
içerisinde openid
belirtilerek bunun OpenID Authentication işlemi ve Identity token için bir istek olduğu belirtilir. Resource owner kimliğini doğruladığında client uygulamasını yetkilendireceği consent sayfasına yönlendirilir. (2) Client uygulaması yetkilendirildiğinde bir authorization code ve identity token üretilerek Redirect Uri‘ye gönderilir. (3) Client uygulaması authorization code karşılığında bir access token ve identity token elde eder. (4) Sonrasındaysa client uygulaması elde ettiği access token ile resource server kaynaklarıyla haberleşmektedir. (5)
Authorization Server
Client Credentials akışında kullanılan Authorization Server projesini geliştirmeye devam edelim. Bu akışta kullanılan kaynakları ekleyelim.
public class Config
{
public static IEnumerable<ApiResource> ApiResources => new List<ApiResource> { new("api") { DisplayName = "Api #1", ApiSecrets = { new("api.secret".Sha256()) }, Scopes = { "api.read", "api.upsert", "api.delete" } } };
public static IEnumerable<ApiScope> ApiScopes => new List<ApiScope> { new("api.read", "Read permission"), new("api.upsert", "Upsert permission"), new("api.delete", "Delete permission") };
public static IEnumerable<Client> Clients => new List<Client>
{
new() { ClientId = "client-app", ClientSecrets = { new("client-app.secret".Sha256()) }, ClientName = "Client Application (Client Credentials)", AllowedGrantTypes = GrantTypes.ClientCredentials, AllowedScopes = { "api.read" } },
new()
{
ClientId = "app",
ClientSecrets = { new("app.secret".Sha256()) },
ClientName = "Client Application (Hybrid)",
AllowedGrantTypes = GrantTypes.Hybrid,
RequirePkce = false,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"api.read",
"api.upsert",
"api.delete",
"CountryAndCity",
"Roles"
},
RedirectUris = { "https://localhost:7003/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:7003/signout-callback-oidc" },
AccessTokenLifetime = 3600,
AllowOfflineAccess = true,
RefreshTokenUsage = TokenUsage.ReUse,
RefreshTokenExpiration = TokenExpiration.Absolute,
AbsoluteRefreshTokenLifetime = 30,
RequireConsent = true
}
};
public static IEnumerable<IdentityResource> IdentityResources => new List<IdentityResource>
{
new IdentityResources.OpenId(), // sub
new IdentityResources.Profile(), // name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at
new IdentityResource
{
Name = "CountryAndCity",
DisplayName = "Country and City",
Description = "User's country and city information",
UserClaims = { "country", "city" }
},
new IdentityResource
{
Name = "Roles",
DisplayName = "Roles",
Description = "User's roles",
UserClaims = { "role" }
}
};
public static List<TestUser> TestUsers => new List<TestUser>
{
new()
{
SubjectId = "1",
Username = "bob",
Password = "password",
Claims = { new("given_name", "Bob"), new("family_name", "Marley"), new("country", "Turkey"), new("city", "Urfa"), new("role", "admin") }
}
};
}
Yeni kaynakların eklendiği görülmektedir. TestUser
ile development ortamında kullanılacak test amaçlı bir kullanıcı ve sahip olduğu claim bilgileri atanmaktadır. IdentityResource
kullanıcı hakkındaki id, name ve email gibi bilgilerden oluşur ve authorization server tarafından desteklenen identity resource bilgilerini temsil eder. Client uygulaması bu identity kaynaklarına erişmek için ilgili kaynakları scope parametresinde belirtir. OpenID Connect spesifikasyonu bir takım standart identity resource tanımlar. Bunlardan bir tanesi zorunlu olan openid kapsamı IdentityResources.OpenId()
kullanılarak eklenmiştir. Identity token içerisinde bulunacak subject id (sub) claim bilgisidir. IdentityResources.Profile()
ile kullanıcı hakkındaki (name, gender gibi) bilgilerin desteklenen identity resource listesine eklendiği görülmektedir. İhtiyaçlar doğrultusunda bir takım özel IdentityResource claim bilgileriyle örneklenerek eklenmiştir.
Hybrid akışını kullanacak Client kaynağı AllowedGrantTypes
property’sine GrantTypes
tiplerinden Hybrid
atanarak eklenmiştir. AllowedScopes
property’siyle client uygulamasının resource owner adına erişebileceği bilgiler tanımlanır. RedirectUris
ve PostLogoutRedirectUris
property’lerinde belirtilen uri’ler client uygulamasına kurulacak NuGet paketinde ön tanımlıdır. RequireConsent
ile consent ekranının resource owner‘a gösterilip gösterilmeyeceği ayarlanmaktadır.
Oluşturulan IdentityResource
ve TestUser
kaynaklarını in-memory olarak ekleyelim.
builder.Services.AddIdentityServer()
.AddInMemoryApiResources(Config.ApiResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddTestUsers(Config.TestUsers)
.AddDeveloperSigningCredential();
Gerekli ön hazırlık yapıldığına göre kullanıcıların giriş, çıkış gibi işlemleri gerçekleştireceği işlevselliğin kazandırılması gerekmektedir. IdentityServer kullanıcı login, logout, consent gibi işlemlerin gerçekleştirildiği bir QuickStart arayüzüne sahiptir. Repository içerisinde belirtildiği şekilde Authorization Server ana dizininde ilgili komutu çalıştıralım. Gerekli dosyalar indirildiğinde proje içerisine QuickStart isminde bir klasör ve Views klasöründe ilgili view‘ların eklendiği görülmelidir. Authorization Server ayağa kaldırılarak TestUser
kaynağındaki kullanıcı credential bilgileriyle giriş yapılarak arayüz incelenebilir.
Client Application
Client uygulamasını temsilen bir MVC projesi oluşturup gerekli IdentityModel
ve OpenID Connect kimlik yetkilendirme işlemlerini gerçekleştirmek için Microsoft.AspNetCore.Authentication.OpenIdConnect
NuGet paketlerini kuralım.
dotnet new mvc -n App
dotnet add package IdentityModel
dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
Sonrasındaysa Cookie ve OpenID Connect Authentication konfigürasyonlarını yapalım.
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; // Cookies
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; // OpenIdConnect
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.AccessDeniedPath = new PathString("/Home/AccessDenied");
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://localhost:7000";
options.ClientId = builder.Configuration.GetValue<string>("Client:Id");
options.ClientSecret = builder.Configuration.GetValue<string>("Client:Secret");
options.ResponseType = "code id_token";
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.Scope.Add("api.read");
options.Scope.Add("offline_access");
options.Scope.Add("CountryAndCity");
options.Scope.Add("Roles");
options.Scope.Add("email");
options.ClaimActions.MapUniqueJsonKey("country", "country");
options.ClaimActions.MapUniqueJsonKey("city", "city");
options.ClaimActions.MapUniqueJsonKey("role", "role");
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "role",
};
});
app.UseAuthentication();
Client uygulaması bir MVC projesi olduğundan Cookie-Based Authentication yapılmaktadır. OpenID Connect Handler AddOpenIdConnect
methoduyla eklenmiştir. ResponseType
property’sine code
id_token
verilerek Hybrid akış kullanılacağı belirtilmiştir. Cookie üzerinde sadece gerekli claim bilgileri tutulmakta ve ilave claim bilgileri için UserInfo
endpoint’inin kullanılması gerekmektedir. GetClaimsFromUserInfoEndpoint
property’siyle ilave claim bilgilerinin bu endpoint üzerinden otomatik olarak çekileceği ayarlanmaktadır. Gerekli scope bilgileri eklenerek oluşturduğumuz IdentityResource içerisindeki claim bilgileri map’lenmektedir. Identity, access ve refresh token bilgileri cookie‘nin properties bölümünde SaveTokens
özelliğiyle saklanmaktadır.
await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
Token bilgilerinin cookie üzerinde saklanmasıyla bu bilgilere yukarıdaki gibi ulaşabiliriz. Bu akışta client uygulamasının yetkilendirilmesiyle elimizde bu token bilgilerinin tamamı olacaktır. Access token kullanarak resource server kaynaklarını tüketelim.
public class ProductsController : Controller
{
public async Task<IActionResult> Index()
{
var httpClient = new HttpClient();
var accessToken = await HttpContent.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
httpClient.SetBearerToken(accessToken);
var response = await httpClient.GetAsync("https://localhost:7001/api/items");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var products = JsonSerializer.Deserialize<List<Product>>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
return View(products);
}
return View(new List<Product>());
}
}
Cookie bilgisi üzerinden access token elde edilerek Authotization Header bilgisine Bearer olarak atanmaktadır. Sonrasındaysa API endpoint’leriyle haberleşilmektedir.
Programatik olarak refresh token kullanarak yeni bir access token elde edecek methodumuzu yazalım.
public async Task RefreshToken()
{
var httpClient = new HttpClient();
var discoveryDocument = await httpClient.GetDiscoveryDocumentAsync("https://localhost:7000");
if (discoveryDocument.IsError) { /* Handle error */ }
var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
var request = new RefreshTokenRequest
{
ClientId = _client.Id,
ClientSecret = _client.Secret,
RefreshToken = refreshToken,
Address = discoveryDocument.TokenEndpoint,
};
var response = await httpClient.RequestRefreshTokenAsync(request);
if (response.IsError) { /* Handle error */ }
var authenticationTokens = new List<AuthenticationToken>
{
new() { Name = OpenIdConnectParameterNames.IdToken, Value = response.IdentityToken },
new() { Name = OpenIdConnectParameterNames.AccessToken, Value = response.AccessToken },
new() { Name = OpenIdConnectParameterNames.RefreshToken, Value = response.RefreshToken },
new() { Name = OpenIdConnectParameterNames.ExpiresIn, Value = DateTime.UtcNow.AddSeconds(response.ExpiresIn).ToString("o", CultureInfo.InvariantCulture) }
};
var authenticationResult = await HttpContext.AuthenticateAsync();
var properties = authenticationResult.Properties;
properties.StoreTokens(authenticationTokens);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, authenticationResult.Principal!, properties);
}
Authorization Server metadata bilgisi GetDiscoveryDocumentAsync
methoduyla elde edilerek cookie üzerinden refresh token bilgisi okunmaktadır. Bu bilgiler doğrultusunda RequestRefreshTokenAsync
methoduyla yeni access token elde edilmektedir. Yeni token bilgileriyle yetkilendirilen kullanıcının cookie üzerindeki token bilgileri güncellenmektedir.
Kullanıcı giriş, çıkış işlemleri gibi işlevsellikler authorization server‘a eklenen QuickStart arayüzleri üzerinden sağlanmaktadır. Yetkilendirme gereken bir sayfaya erişilmeye çalışıldığında kullanıcı otomatik olarak Authorization Server kimlik doğrulama sayfasına yönlendirilmektedir. Çıkış işlemleri için bizim yönlendirme yapmamız gerekmektedir.
public async Task Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}
İlk satırda client uygulamasından çıkış işlemi yapılmaktadır. İkinci satırda kullanıcı authorization server‘a yönlendirilerek çıkış işlemi buradan gerçekleştirilmektedir. Bu işlemle kullanıcıya çıkış işleminin başarılı olduğunu söyleyen bir sayfa gösterilmektedir. Bu davranış AccountOptions
sınıfındaki AutomaticRedirectAfterSignOut
field’ı üzerinden değiştirilmektedir.
Client Application Role-Based Authorization
Bu akışı bitirmeden controller sınıfımızın action methodlarını koruma altına alalım. Authorization server tarafından desteklenen IdentityResource
kaynakları tanımlanırken Roles isimli özel bir IdentityResource tanımlanmıştı. Sonrasındaysa client kaynağının scope bilgisine bu identity kaynağı verilerek resource owner adına bu bilgiye erişeceği belirtilmişti. TestUser
kullanıcıları oluşturulurken Claim bilgisine sahip olduğu rol bilgisi atanmıştı.
Client uygulamasında AddOpenIdConnect
methoduyla rol için hangi isimlendirmenin kullanılacağı TokenValidationParameters
ile belirtilmişti. Bu konfigürasyonlardan sonra yapılması gereken tek şey action methodlarının Authorize
attribute’üyle işaretlenmesidir.
public class UserController : Controller
{
[Authorize(Roles = "admin")]
public IActionResult Dasboard() => View();
[Authorize(Roles = "admin, customer")]
public IActionResult Customers() => View();
}
Bu sayfalara gerekli rollere sahip olmayan kullanıcılar erişim sağlayamayacaktır.
Implicit Flow
Bu akış son zamanlara kadar JavaScript tabanlı client uygulamaları için önerilen akıştı. Uygulama kaynak kodları erişime açık olduğundan ClientSecret
bilgisini bünyesinde tutamaz. Bu akışta authorization code değiş tokuşu olmaksızın client uygulamasına tek işlemde access token dönülmektedir.
Son zamanlarda yeni standartlarla bu durum değişmiştir. JavaScript tabanlı client uygulamaları için Authorization Code Flow with PKCE (Proof Key for Code Exchange) akışının kullanılması tavsiye edilmektedir.
Authorization Code Flow with PKCE
Authorization Code akışında Hybrid akışa göre Response Type değerinin code
olması gibi ufak farklılıklar bulunmaktadır. Bu akış client uygulamasının kullanıcıyı authorization server‘a yönlendirmesiyle başlar. Client uygulamasının yetkilendirilmesiyle IdP, front-channel üzerinden geriye bir authorization code döner. Client uygulaması Token
endpoint’ini kullanarak code
karşılığında back-channel üzerinden access token elde eder. Sonrasındaysa resource server kaynaklarıyla access token kullanarak iletişime geçer.
PKCE kullanıldığı takdirde akış biraz daha değişmektedir.
Client uygulaması resource owner‘ı authorization server‘a şifrelenmiş code_challenge
ile yönlendirir. (1) IdP bünyesinde bunu depolamaktadır. Resource owner kimliğini doğrulandığında client uygulamasını yetkilendirmek üzere consent ekranına yönlendirilmektedir. (2) Client uygulaması yetkilendirildiğinde geriye authorization code
dönülür. (3) Client sonrasındaysa code
ve code_verifier
bilgisini Token
endpoint’ine back-channel üzerinden iletir. (4) IdP code_verifier
bilgisini şifreleyerek code_challenge
ile karşılaştırıp geriye id token ve access token döner.
Authorization Server
IdentityServer projesini geliştirmeye devam edelim. Bu akışta ilave bir client eklememiz yeterlidir.
public class Config
{
public static IEnumerable<Client> Clients => new List<Client>
{
new()
{
ClientId = "angular-app",
ClientName = "Angular App (JavaScript Client)",
AllowedGrantTypes = GrantTypes.Code,
RequireClientSecret = false,
RequirePkce = true,
AllowAccessTokensViaBrowser = true,
RequireConsent = false,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"api.read",
"CountryAndCity",
"Roles"
},
AccessTokenLifetime = 600,
AllowedCorsOrigins = { "http://localhost:4200" },
RedirectUris = { "http://localhost:4200/signin-callback" },
PostLogoutRedirectUris = { "http://localhost:4200/signout-callback" }
}
};
}
Client kaynağına AllowedGrantTypes
özelliğine GrantTypes
tiplerinde Code
atanarak oluşturulmuştur. PKCE‘nin gerekli olduğu RequirePkce
ile front-channel üzerinden access token dönüleceği AllowAccessTokensViaBrowser
ile belirtilmiştir. CORS origin ayarlamaları, giriş ve çıkış işlemlerinde kullanılan Redirect URI adresleri tanımlanmıştır.
Client Application
Bu akışta client uygulaması için Angular projesi oluşturulacaktır. Projeyi oluşturup gerekli oidc-client
npm paketini kuralım ve tüm işlemleri gerçekleştireceğimiz bir component oluşturalım.
ng new AngularApp
npm install oidc-client
ng generate component Home
Öncelikle environment dosyalarında kullanılan gerekli bilgileri tanımlayalım.
export const environment = {
stsAuthority: 'https://localhost:7000',
clientId: 'angular-app',
clientRoot: 'http://localhost:4200',
clientScope: 'openid profile api.read CountryAndCity Roles',
apiRoot: 'https://localhost:7001/api'
};
Sonrasındaysa giriş, çıkış gibi işlemleri gerçekleştirecek servisimizi yazalım.
import { User, UserManager } from 'oidc-client';
@Injectable({ providedIn: 'root' })
export class AuthService {
userManager: UserManager;
constructor() {
this.userManager = new UserManager({
authority: environment.stsAuthority,
client_id: environment.clientId,
redirect_uri: `${environment.clientRoot}/signin-callback`,
silent_redirect_uri: `${environment.clientId}/silent-callback`,
post_logout_redirect_uri: environment.clientRoot,
response_type: 'code',
scope: environment.clientScope
});
}
getUser = () => this.userManager.getUser();
login = () => this.userManager.signinRedirect();
logout = () => this.userManager.signoutRedirect();
renewToken = () => this.userManager.signinSilent();
isAuthenticated = async () => this.checkUser(await this.userManager.getUser());
private checkUser = (user: User | null) => !!user && !user.expired;
}
Client uygulamasının yetkilendirilmesiyle belirtilen Redirect URI adresine callback olduğunda gerçekleşecek işlemleri yazalım.
import { UserManager } from 'oidc-client';
@Component({ selector: 'signin-callback', template: '', styles: [] })
export class SigninCallbackComponent implements OnInit {
constructor(private router: Router) { }
ngOnInit(): void {
new UserManager({ response_mode: 'query' }).signinRedirectCallback().then(() => {
this.router.navigate(['/']);
}, error => console.error(error));
}
}
Sonrasındaysa kullanıcıların giriş, çıkış yapacağı ve resource server kaynaklarıyla konuşacağı kod bloğunu HomeComponent
içerisinde tanımlayalım.
import { User } from 'oidc-client';
@Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.css'] })
export class HomeComponent implements OnInit {
currentUser: User | null = null;
items: Product[] = [];
constructor(private authService: AuthService, private http: HttpClient) { }
async ngOnInit() {
this.currentUser = await this.authService.getUser();
}
onLogin = () => this.authService.login();
onLogout = () => this.authService.logout()
onRenewToken = async () => {
this.currentUser = await this.authService.renewToken();
}
getProducts = () => {
this.http.get<Product[]>(`${environment.apiRoot}/items`, { headers: { 'Authorization': `Bearer ${this.currentUser.access_token}` }}).subscribe(res => {
this.items = res;
});
}
}
Projeler ayağa kaldırıldığında kullanıcının login butonuna tıklamasıyla authorization server’a yönlendirildiği görülecektir. Giriş işleminin başarılı olmasıyla kullanıcı yetkilendirilerek Angular uygulamasına geri dönülmektedir. Client uygulaması artık API kaynağına access token ile giderek izni doğrultusunda istediği bilgilere erişmektedir.
Resource Owner Password Flow
Bu akış client uygulaması ve authorization server arasında güven ilişkisi olduğu durumlarda tercih edilmelidir. Öncelikle resource owner, credential bilgilerini authorization server yerine client uygulamasına girmektedir. Client uygulaması elde ettiği credential bilgileriyle back-channel üzerinden authorization server‘a giderek access token elde eder. Taraflar arasında güven ilişkisi olmadığı durumlarda client uygulamasının credential bilgilerini nasıl işlediği bilinemeyecektir.
Authorization Server
IdentityServer projesine yeni bir client ekleyerek geliştirmeye devam edelim.
public class Config
{
public static IEnumerable<Client> Clients => new List<Client>
{
new()
{
ClientId = "pwd-app",
ClientSecrets = { new("pwd-app.secret".Sha256()) },
ClientName = "Client Application (Resource Owner Password)",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
IdentityServerConstants.StandardScopes.Email,
"api.read",
"api.upsert",
"api.delete",
"CountryAndCity",
"Roles"
},
AccessTokenLifetime = 3600,
AllowOfflineAccess = true,
RefreshTokenUsage = TokenUsage.ReUse,
RefreshTokenExpiration = TokenExpiration.Absolute,
AbsoluteRefreshTokenLifetime = 30,
}
};
}
Client uygulamasının akış tipi GrantTypes
tiplerinden ResourceOwnerPassword
olarak belirtilmiştir. Bu akışın kullanılabilmesi için IResourceOwnerPasswordValidator
arayüzünün implemente edilmesi gerekmektedir.
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
var user = Config.TestUsers.FirstOrDefault(f => f.Username == context.UserName && f.Password == context.Password);
if (user != null)
{
context.Result = new GrantValidationResult(user.SubjectId, OidcConstants.AuthenticationMethods.Password);
}
return Task.CompletedTask;
}
}
Görüldüğü üzere ResourceOwnerPasswordValidationContext
nesnesi üzerinden TestUser
kaynağındaki kullanıcı credential bilgileri karşılaştırılmaktadır. Şartların karşılanmasıyla sonuç GrantValidationResult
olarak belirlenmektedir.
builder.Services.AddIdentityServer()
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>();
Oluşturduğumuz validator sınıfının IdentityServer‘a eklenmesi gerekmektedir.
Client Application
Client projesi için bir MVC projesi oluşturarak gereken IdentityModel
NuGet paketini kuralım.
dotnet new -n PwdApp
dotnet add package IdentityModel
Program.cs
içerisinde Cookie Authentication konfigürasyonlarını gerçekleştirelim.
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.LoginPath = new PathString("/User/Login");
options.LogoutPath = new PathString("/User/Logout");
options.AccessDeniedPath = new PathString("/Home/AccessDenied");
});
app.UseAuthentication();
Client uygulaması içerisinden kullanıcın giriş ve çıkış işlemlerini gerçekleştireceği controller sınıfını dolduralım.
[Authorize]
public class UserController : Controller
{
private readonly Client _client;
public UserController(IOptions<Client> client)
{
_client = client.Value;
}
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel viewModel)
{
if (ModelState.IsValid)
{
var httpClient = new HttpClient();
var discoveryDocument = await httpClient.GetDiscoveryDocumentAsync("https://localhost:7000");
if (discoveryDocument.IsError) { /* Handle error */ }
var request = new PasswordTokenRequest
{
Address = discoveryDocument.TokenEndpoint,
UserName = viewModel.UserName,
Password = viewModel.Password,
ClientId = _client.Id,
ClientSecret = _client.Secret
};
var response = await httpClient.RequestPasswordTokenAsync(request);
if (response.IsError) { /* Handle error */ }
var userInfoRequest = new UserInfoRequest
{
Token = response.AccessToken,
Address = discoveryDocument.UserInfoEndpoint,
};
var userInfoResponse = await httpClient.GetUserInfoAsync(userInfoRequest);
if (userInfoResponse.IsError) { /* Handle error */ }
var identity = new ClaimsIdentity(userInfoResponse.Claims, CookieAuthenticationDefaults.AuthenticationScheme, "name", "role");
var principle = new ClaimsPrincipal(identity);
var authenticationProperties = new AuthenticationProperties();
authenticationProperties.StoreTokens(new List<AuthenticationToken>
{
new() { Name = OpenIdConnectParameterNames.AccessToken, Value = response.AccessToken },
new() { Name = OpenIdConnectParameterNames.RefreshToken, Value = response.RefreshToken },
new() { Name = OpenIdConnectParameterNames.ExpiresIn, Value = DateTime.UtcNow.AddSeconds(response.ExpiresIn).ToString("o", CultureInfo.InvariantCulture) },
});
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principle, authenticationProperties);
return RedirectToAction("Index");
}
return View(viewModel);
}
public async Task Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
IdentityServer metadata bilgileri çekilerek RequestPasswordTokenAsync
methoduyla kullanıcı giriş yaptırılmaya çalışılmaktadır. Giriş işleminin başarılı olmasıyla GetUserInfoAsync
methoduyla kullanıcı bilgileri UserInfo
endpoint’inden çekilmektedir. Bu bilgilerle ClaimsPrincipal sınıfı örneklenerek kullanıcı client uygulamasında giriş yaptırılmaktadır.
IdentityServer Custom UserStore
IdentityServer projemizi test kullanıcılarından ayırarak gerçek hayat senaryolarına biraz daha yakınlaştıralım. Öncelikle Microsoft.EntityFrameworkCore.SqlServer
NuGet paketini kuralım ve kullanıcı sınıfımızı oluşturalım.
public class AppUser
{
public int Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
DbContext
sınıfımızı oluşturalım.
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<AppUser> Users => Set<AppUser>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AppUser>().HasData(
new AppUser { Id = 1, UserName = "bob", Email = "[email protected]", Password = "1234", City = "New Jersey" },
new AppUser { Id = 2, UserName = "alice", Email = "[email protected]", Password = "1234", City = "London" }
);
base.OnModelCreating(modelBuilder);
}
}
DbContext
sınıfımızı servis olarak ekleyelim.
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer"));
});
AppUser
sınıfı üzerinde bir takım işlemleri gerçekleştiren Repository
sınıfımızı yazalım. IRepository
arayüzü yazıyı uzatmamak için paylaşılmamıştır. Sonrasındaysa bu sınıfı AddScoped
methoduyla DI Container‘a ekleyelim.
public class UserRepository : IUserRepository
{
private readonly AppDbContext _context;
public UserRepository(AppDbContext context)
{
_context = context;
}
public async Task<AppUser?> GetByUserNameAsync(string userName) => await _context.Users.FirstOrDefaultAsync(f => f.UserName == userName);
public async Task<AppUser?> GetByIdAsync(int id) => await _context.Users.FirstOrDefaultAsync(f => f.Id == id);
public async Task<bool> ValidateAsync(string email, string password) => await _context.Users.AnyAsync(f => f.Email == email && f.Password == password);
}
Resource Owner Password akışında IResourceOwnerPasswordValidator
arayüzü implemente edilmişti. Bu sınıfı oluşturulan Repository
sınıfına göre düzenleyelim.
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private readonly IUserRepository _userRepository;
public ResourceOwnerPasswordValidator(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
var isValid = await _userRepository.ValidateAsync(context.UserName, context.Password);
if (isValid)
{
var user = await _userRepository.GetByEmailAsync(context.UserName);
context.Result = new GrantValidationResult(user!.Id.ToString(), OidcConstants.AuthenticationMethods.Password);
}
}
}
Görüldüğü üzere kullanıcı credential bilgileri kontrol edilerek sonuç Result
özelliği üzerinden atanmaktadır.
TestUser
kaynağında test kullanıcılarının claim bilgileri oluşturulurken atanmıştı. Bu claim bilgilerine değinildiği üzere token ve UserInfo
endpoint’i üzerinden erişilmektedir. Custom UserStore implementasyonuyla kullanıcılar dinamik şekilde elde edileceğinden claim atamaları da dinamik olmalıdır. Başarılı giriş işlemlerinde kullanıcılara claim atamak için IProfileService
arayüzü implemente edilmelidir.
public class ProfileService : IProfileService
{
private readonly IUserRepository _userRepository;
public ProfileService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var userId = context.Subject.GetSubjectId();
var user = await _userRepository.GetByIdAsync(int.Parse(userId));
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Email, user.Email),
new("name", user.UserName)
};
if (user.UserName == "bob") claims.Add(new("role", "admin"));
else claims.Add(new("role", "customer"));
context.AddRequestedClaims(claims);
}
public async Task IsActiveAsync(IsActiveContext context)
{
var userId = context.Subject.GetSubjectId();
var user = await _userRepository.GetByIdAsync(int.Parse(userId));
context.IsActive = user != null;
}
}
Arayüz iki methodla birlikte gelmektedir. Kullanıcının token almasına izin verilip verilmeyeceği IsActiveAsync
methoduyla belirtilir. Claim bilgileri kullanıcıya GetProfileDataAsync
methoduyla atanmaktadır. Kullanıcı id bilgisi GetSubjectId
methoduyla elde edilerek Repository üzerinden kullanıcı elde edilmektedir. Sonrasındaysa claim bilgileri oluşturularak AddRequestedClaims
methoduyla kullanıcıya atanmaktadır.
ProfileService
sınıfını IdentityServer‘a ekleyelim.
builder.Services.AddIdentityServer()
.AddProfileService<ProfileService>();
QuickStart ile eklenen UI içerisinde son bir değişikliğin daha yapılması gerekmektedir. Artık test kullanıcılarıyla çalışmayacağımız için AccountController
içerisinden TestUserStore
sınıfı IUserRepository
arayüzüyle değiştirilmelidir.
private readonly IUserRepository _userRepository;
public AccountController(IUserRepository userRepository)
{
_userRepository = userRepository;
}
Http POST Verb‘ine ait Login
action methodunda aşağıdaki kod bloğunun,
if (_users.ValidateCredentials(model.Username, model.Password))
{
var user = _users.FindByUsername(model.Username); // in-memory store
// ...
}
Şeklinde değiştirilmesi gerekmektedir.
if (await _userRepository.ValidateAsync(model.Email, model.Password))
{
var user = await _userRepository.GetByEmailAsync(model.Email); // custom store
// ...
}
IdentityServer ASP.NET Identity
IdentityServer projesine ASP.NET Identity desteği kazandırmanın iki yolu vardır. Geliştirmeye sıfırdan başlanıyorsa hazır bir template üzerinden aşağıdaki gibi proje oluşturulabilir.
dotnet new --install Duende.IdentityServer.Templates
dotnet new isaspid -n AuthServer
Mevcut bir IdentityServer projesine ASP.NET Identity desteğini EF Core implementasyonu Microsoft.AspNetCore.Identity.EntityFrameworkCore
NuGet paketini kurarak başlayalım. Sonrasındaysa kullanıcı sınıfımızı oluşturalım.
public class AppUser : IdentityUser { }
DbContext
sınıfımızı IdentityDbContext<TUser>
sınıfından inherit alarak oluşturalım.
public class AppDbContext : IdentityDbContext<AppUser>
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
}
Oluşturduğumuz DbContext sınıfını, Identity API ve IdentityServer’i ekleyelim.
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer"));
});
builder.Services.AddIdentity<AppUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddIdentityServer()
.AddAspNetIdentity<AppUser>()
.AddDeveloperSigningCredential();
app.UseIdentityServer();
ASP.NET Identity implementasyonu bu kadar basittir. IdentityServer AddAspNetIdentity
methoduyla IUserClaimsPrincipalFactory
IResourceOwnerPasswordValidator
ve IProfileService
arayüzlerini ASP.NET Identity implementasyonuna göre yapılandırır. Bu arayüzler Custom UserStore başlığında olduğu gibi implemente edilmektedir.
IdentityServer Entity Framework Core
IdentityServer kaynakları şimdiye değin in-memory olarak kullanıldı. Bu kaynaklar değiştirildiğinde projenin tekrar ayağa kaldırılması gerekmektedir. Bu durumda dağıtılan refresh token, üretilen authorization code bilgileri geçersiz olacaktır. Bu kaynakların veritabanına taşınmasıyla persistentcy sağlanabilir. Bu işlem için IdentityServer4.EntityFramework
NuGet paketinin kurulması gerekmektedir. Bu paket aşağıdaki DbContext
sınıflarını uygular.
- ConfigurationDbContext Client, ApiResource, ApiScope gibi kaynaklar için kullanılmaktadır.
- PersistedGrantDbContext Refresh Token, Authorization Code gibi geçici veriler için kullanılmaktadır.
Bu veriler SQL Server üzerinde depolanacağından Microsoft.EntityFrameworkCore.SqlServer
NuGet paketinin kurulması gerekmektedir.
var assemblyName = typeof(Program).GetTypeInfo().Assembly.GetName().Name;
builder.Services.AddIdentityServer()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = context =>
{
context.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer"), action =>
{
action.MigrationsAssembly(assemblyName);
});
};
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = context =>
{
context.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer"), action =>
{
action.MigrationsAssembly(assemblyName);
});
};
});
Görüldüğü üzere verileri taşımak bu kadar kolay. ConfigurationDbContext
sınıfı AddConfigurationStore
methoduyla ve PersistedGrantDbContext
sınıfı AddOperationalStore
methoduyla eklenmektedir.