.Net 6.0 ve IdentityServer v4 ile Merkezi Login Sistemi (SSO)
Bu gönderide .bir net kütüphanesi olan identityserver ile authentacion işlemlerimizi yaptığımız merkezi bir giriş sistemi yapacağız ve farklı clientlarımız bu ortak girişi kullanacak.
SSO (Single Sign-On) Nedir?
Kimlik ve parola bilgilerimizin tek bir tarafta kontrol edilip tek bir yerden oturum açmamıza imkan sağlar. Mesela google, microsoft, facebook gibi şirketlerin farklı uygulamaları için yeni bir kullanıcı yada şifre eklemiyoruz tek bir kullanıcı adı ve şifre ile diğer uygulamalara erişebiliyoruz.

Projemizin yapısı
İki farklı proje açacağız, biri merkezi giriş yapacağımız .net 6 ve identityserver kullandığımız proje diğeri de .net mvc client’ımız olacak
Geliştirme Ortamı
- Windows 11
- Visual Studio 2022
- .Net 6.0
Projenin Başlangıcı
SSO için login page ve diğer identity işlemleri bu projede olacağından .net 6.0 mvc seçenekleriyle projemi oluşturuyorum.

Kullanıcılarımızı saklayacağımız bir veritabanına ihtiyacımız var bunun için öncelikle entityframework ve veritabanı ayarlarını yapmalıyız.
Aşağıdaki paketleri yükleyelim :
1 2 3 |
Install-Package Microsoft.EntityFrameworkCore Install-Package Microsoft.EntityFrameworkCore.SqlServer Install-Package Microsoft.EntityFrameworkCore.Tools |
EntityFrameworkCore.SqlServer : veritabanı sağlayıcımız sql server dependy injection yapar,
EntityFrameworkCore.Tools : migration komutları için.
DbContext dependy injection /Program.cs
1 2 |
builder.Services.AddDbContext<ApplicationContext>( options => options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServer"))); |
DbContext‘ten miras alınan context sınıfını burada kaydediyoruz, veritabanı sağlayıcısı olarak da Sql Server‘ı ekledik.
ApplicationContext.cs
AddDbContext kısmında options nesnesini kullanabilmek için Context sınıfının constrcutorında mutlaka DbContextOptions<T> implement etmeleyiz.
1 2 3 4 5 |
public class ApplicationContext : DbContext { public ApplicationContext(DbContextOptions<ApplicationContext> context) : base(context) { } } |
Kullanıcı entitysini tanımlayalım: User.cs
1 2 3 4 5 6 7 8 9 |
public class User { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Username { get; set; } public string Password { get; set; } } |
Id otomatik artan şeklinde tanımladık.
SqlServer için appSetting.json‘da connection stringini yazalım.
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "ConnectionStrings": { "SqlServer": "Server=(localdb)\\mssqllocaldb;Database=SingleSignOn;Trusted_Connection=True;" }, "AllowedHosts": "*" } |
ApplicationContext’e user’ı ekleyelim.
1 |
public DbSet<User> Users { get; set; } |
Migration kısmına geçerek veritabanını oluşturabiliriz
1 2 |
add-migration initial update-database |
migrationlarla ilgili daha fazla bilgi için şu linke göz atabilirsiniz.
Sql Server Management Studio ile veritabanını kontrol edebiliriz

IdentityServer
Ana konumuz olan identity server ayarlarını yapalım. Öncelikle aşağıdaki paketleri yükleyelim.
1 2 3 |
Install-Package IdentityServer4 Install-Package IdentityServer4.EntityFramework Install-Package IdentityServer4.Storage |
Program.cs’ye identity server ile ilgili aşağıdaki ayarları ekleyelim
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var conStr = builder.Configuration.GetConnectionString("SqlServer"); var assembly = typeof(Program).Assembly.GetName().Name; builder.Services.AddIdentityServer() .AddConfigurationStore( options => options.ConfigureDbContext = config => config.UseSqlServer(conStr, opt => opt.MigrationsAssembly(assembly)) ) .AddOperationalStore( options => options.ConfigureDbContext = config => config.UseSqlServer(conStr, opt => opt.MigrationsAssembly(assembly)) ).AddDeveloperSigningCredential(); ... ... ... app.UseIdentityServer(); |
iki tane store edeceğimiz bilgileri ekledik, Bu store’ların kendine ait contextleri var.
- ConfigurationStore: adından anlaşılacağı gibi clients, resources ve cors gibi ayarları depolamaya yarar
- OperationalStore: grants, consents, and token gibi authorization ayarlarını saklıyor
AddDeveloperSigningCredential: IdentityServer doğrulama ve imzalama için bazı anahtarlamalar kullanıyor, Bu anahtarlarda RS256, RS384 gibi algoritmalarla şifrelenir, bazı metotlarla da bu anahtar configüre edilebilir. AddSigningCredential, AddDeveloperSigningCredential, AddValidationKey gibi düzenlemeler var biz localhost için olan AddDeveloperSigningCredential’i kullanacağız.
migration:
1 |
add-migration identityServerConf -c PersistedGrantDbContext |
1 |
add-migration identityServerConf -c ConfigurationDbContext |
update-database
1 2 |
update-database -Context ConfigurationDbContext update-database -Context PersistedGrantDbContext |
migration işlemlerinden sonra tablolarımızın oluştuğumu göreceğiz, bu tablolarla ilgili detayları aşağıda belirtmiş olacağım. Bu oluşturduğumuz tabloları dolduracak bir seed data yazalım. Bu datalar üzerinden identityserver’ın objelerini yakından inceleyelim.
Identity Resources
Username,mail,id gibi unique user/identity datalardır. buraya ekstra datalar eklenebilir (role gibi)
1 2 3 4 5 |
public static IEnumerable<IdentityResource> IdentityResources => new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; |
Client
IdentityServer’dan token talep eden uygulamalardır. Bu uygulamalar web, mobile, spa, server gibi yazılım teknolojileridir.
Bir statik class üzerinde örnek bir client tanımlayalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public static IEnumerable<Client> Clients => new List<Client> { new() { ClientId = "mvc-app", ClientName = "Mvc App", AllowOfflineAccess = true, RequirePkce = true, AllowedGrantTypes = GrantTypes.Code, ClientSecrets = { new("MvcApp".Sha256())}, AllowedScopes = {"openid","profile","roles"}, RedirectUris = {"https://localhost:7204/signin-oidc"}, FrontChannelLogoutUri = "https://localhost:7204/signout-oidc", PostLogoutRedirectUris = { "https://localhost:7204/Home/Index" }, RequireConsent = false, } }; |
Örnek bir tane client ekledik, AllowedGrantTypes özelliği client’ın nasıl kullanılacağını belirleyen önemli bir parametre, bu özellik eğer code olarak set edilirse client’a bir kullanıcı aracılığıyla bir giriş yapılacağını belirtmiş oluruz. Verilen Url’ler uygulamanın kendi url’leri olacak şekildedir.
Bu statik verilerimizi middleware tarafında identity server’ın memory’sine tanımlayalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
builder.Services.AddIdentityServer() .AddConfigurationStore( options => options.ConfigureDbContext = config => config.UseSqlServer(conStr, opt => opt.MigrationsAssembly(assembly)) ) .AddOperationalStore( options => options.ConfigureDbContext = config => config.UseSqlServer(conStr, opt => opt.MigrationsAssembly(assembly)) ) .AddInMemoryApiResources(IdentityConfig.ApiResources) .AddInMemoryApiScopes(IdentityConfig.ApiScopes) .AddInMemoryClients(IdentityConfig.Clients) .AddInMemoryIdentityResources(IdentityConfig.IdentityResources) .AddDeveloperSigningCredential(); |
Login Controller/View
Merkezi login controllerı ve view’ını identityserver projesine ekleyelim.
AccountController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class AccountController : Controller { private readonly IUserService _userService; public AccountController(IIdentityServerInteractionService interaction,IUserService userService) { _userService = userService; } public IActionResult Login(string returnUrl) { return View(new LoginViewModel { ReturnUrl = returnUrl }); } [HttpPost] public async Task<IActionResult> Login(LoginViewModel model) { var user = _userService.GetByUsername(model.UserName); if (user == null) { ModelState.AddModelError("UserName", "User or password are not correct"); return View(model); } //could do password check; var identityServerUser = new IdentityServerUser(user.Id.ToString()); await HttpContext.SignInAsync(identityServerUser); return Redirect(model.ReturnUrl); } } |
UserService işlevi user,password vs gibi kontroller sizde burada istediğiniz düzenlemeleri, validasyonları ekleyebilirsiniz. Login ekranına ise aşağıdaki github adresinden ulaşabilirsiniz.
Cookie Ayarları
Identity sitemiz’de login olduktan sonra bilgiler cookie’ye set edilir, bu çerezlerden diğer sitelerimizin erişebilmesi için bazı ayarları yapmamız gerekiyor.
1 2 3 4 5 6 |
builder.Services.ConfigureApplicationCookie(options => { options.Cookie.Name = "IdentityServer.Cookie"; options.LoginPath = "Account/Login"; options.LogoutPath = "Account/Logout"; }); |
1 2 3 4 5 6 |
app.UseCookiePolicy(new CookiePolicyOptions { HttpOnly = HttpOnlyPolicy.Always, MinimumSameSitePolicy = SameSiteMode.Lax, Secure = CookieSecurePolicy.Always }); |
IdentityProfileService
Burada kritik bir noktaya geldik, kullanıcımız login olduktan sonra account/login post olacak orada bir cookie ataması yapıyoruz. ama tabiki bu kadar atama yeterli değil kullanıcının ismi, maili, doğum tarihi vs. gibi bilgileri istenilen clienta vermeliyiz. Bunun için identityserver’ın sağladığı IProfileService den miras alınarak bir service yazalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public class IdentityProfileService : IProfileService { private readonly IUserService _userService; public IdentityProfileService(IUserService userService) { _userService = userService; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var userId = context.Subject.GetSubjectId(); if (userId.IsNullOrEmpty()) { return; } var user = _userService.GetById(int.Parse(userId)); if (user == null) { return; } var claims = new List<Claim>() { new Claim(JwtClaimTypes.Gender,user.Gender), new Claim(JwtClaimTypes.PhoneNumber,"+507"), new Claim(JwtClaimTypes.Name,user.Name), new Claim(JwtClaimTypes.Email,user.Email), new Claim(JwtClaimTypes.BirthDate,user.BirthDate.ToString()), new Claim(JwtClaimTypes.Id,user.Id.ToString()), }; user.Roles.ForEach(r => claims.Add(new Claim(JwtClaimTypes.Role, r))); var requestedClaims = claims.Where(p => context.RequestedClaimTypes.Contains(p.Type)).ToList(); context.IssuedClaims = requestedClaims; context.AddRequestedClaims(requestedClaims); } public async Task IsActiveAsync(IsActiveContext context) { var data = context.IsActive; await Task.CompletedTask; } } |
Giriş yapmak isteyen client’ın talep ettiği ve yetkisi olan claimler burada eklenecek. Dependecy injection için AddProfileService()<IdentityProfileService>() eklemeyi unutmayın
Mvc uygulması
Şimdi bir tane .net core mvc projesi ekleyelim ve login olarak identity server’ı ekleyelim.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = "oidc"; //OpenIdConnect }) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => { options.ExpireTimeSpan = TimeSpan.FromHours(24); }) .AddOpenIdConnect("oidc", options => { options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.ClientId = "mvc-app"; options.ClientSecret = "MvcApp"; options.Authority = "https://localhost:7267/"; options.ResponseType = "code"; options.SaveTokens = true; options.SignedOutCallbackPath = "/Home/Index"; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("roles"); options.GetClaimsFromUserInfoEndpoint = true; options.ClaimActions.MapJsonKey("role", "role", "role"); options.ClaimActions.MapUniqueJsonKey("gender", "gender", "gender"); options.TokenValidationParameters.RoleClaimType = "role"; options.TokenValidationParameters = new TokenValidationParameters { AuthenticationType = CookieAuthenticationDefaults.AuthenticationScheme }; options.Events = new OpenIdConnectEvents { OnTokenValidated = async context => { var identity = context.Principal?.Identity as ClaimsIdentity; if (identity == null) { return; } identity.AddClaim(new Claim("customClaim", "customValue"));//custom claim } }; }); |
MapJsonKey gelen claimleri çözümlemesi gerekli bir fonksiyon sadece role ve gender için var o yüzden phone gibi alanlar yetkisi olunsa bile cookie’ye eklenmez.
OnTokenValidated ise artık giriş işlemleri başarılı olduktan sonra yakalanan event. Burada kendi client’ınıza özel cliamleri ekleyebilirsiniz.
Authority: SSO adresi
1 2 |
app.UseAuthentication(); app.UseAuthorization(); |
Middleware yukarıdaki ayarları eklemeyi unutmayın.
Identity server içinde MvcApp için url bilgilerinde mutlaka ayağa kalkan mvc uygulamanızın giriş bilgilerini giriniz.
Şimdi hem mvc uygulaması içinde bir endpoinete [Authorize] attribute ekleyelim.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class HomeController : Controller { private readonly ILogger<HomeController> _logger; public HomeController(ILogger<HomeController> logger) { _logger = logger; } public IActionResult Index() { return View(); } [Authorize] public IActionResult Privacy() { return View(); } [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } } |
Privacy tıkladığımız an bizi login page’e atması gerekecek.


Logout
Öncelikle account controller’a bir logout endpointi eklemeliyiz.
IdentityServer -> AccountController -> Logout
1 2 3 4 5 6 7 8 9 |
public async Task<IActionResult> Logout(string logoutId) { var client = await _interaction.GetLogoutContextAsync(logoutId); if (string.IsNullOrEmpty(client.PostLogoutRedirectUri)) { return RedirectToAction("Index", "Home"); } return Redirect(client.PostLogoutRedirectUri); } |
MvcApp -> Logout
1 2 3 4 |
public IActionResult Logout() { return SignOut("Cookies", "oidc"); } |
Statik identity config üzerinde PostLogoutRedirecturis alanını doğru yazmayı unutmayınız
Sonuç
Merkezi bir sso ve bunu tüketecek bir tane de mvc uygulaması yaptık. Sql ayarlarını yapsak da verileri memory üzerinden işledik. Uygulama’da kritik nokta cookiler’i paylaşma, bu paylaşma’da Client’ın yetkili olduğu cliamlere göre yapıyoruz bunu da ProfileService içinde yaptık.
İşler karışık gibi gözükse de olay giriş yapma, yönlendirme ve cookie’leri paylaşmadan ibaret.
Proje Linki: https://github.com/okankrdg/SingleSignOn