IdentityServer Implementation

.NET 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.

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.

IdentityServer Client Credentials Grant

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.

FlowResponse Type
Authorization Codecode
Implicitid_token
Implicitid_token token
Hybridcode id_token
Hybridcode token
Hybridcode 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.

IdentityServer Hybrid Flow

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.

IdentityServer Implicit Flow

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.

IdentityServer Authorization Code Flow with PKCE

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.

You may also like...

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir