IdentityServer4 İle Windows Authentication Uygulaması

IdentityServer4 + Windows Auth

Geliştirdiğimiz uygulamaların, belirli bir domain veya kuruluş içerisinde çalışması gerekebilir. Domain içerisinde bulunan kullanıcıların kullanıcı adı ve şifre  ile uygulamaya giriş yapmasını (Login) istemiyorsak,  kullanıcıları kimliklendirme için windows authentication’ı kullanabiliriz. Bu yazımda IdentityServer4 üzerinden, client uygulamamızın windows authentication kullanarak kullanıcı doğrulama yapmasını anlatmaya çalışacağım. Bir önceki IdentityServer4 ile client credential örneğinde oluşturduğum IdentityServer uygulaması üzerinde windows authentication için gerekli yapılandırmayı oluşturduktan sonra bu yapılandırma için bir client uygulama oluşturulmasını anlatmaya çalışacağım.

Yapılacak işlem adımları sırasıyla;

  • IdentityServer uygulaması tarafında client ve varsa api (resource) için konfigürasyonların Config.cs dosyasına eklenmesi.
  • IdentityServer tarafında Startup.cs dosyasında Identityserver ve windows authentication yapılandırılmasının yapılması.
  • IdentityServer uygulaması tarafında kullanıcı, claim veya role bilgilerinin kaydedilmesi için standart veri tabanı tablolarının oluşturulması ( Migrations )
  • ExternalController üzerinde düzenleme
  • Eğer client uygulamamız bir api ( resource ) kullanıyorsa, bu uygulamanın oluşturulması ve auth. için yapılandırılması
  • Client uygulamamızın oluşturulması ve Authentication için yapılandırılması

IdentityServer4

Client Ve Resource Yapılandırma:

Resource (api) uygulamamızı ve bu resource’i kullanmak isteyen client uygulamamızı, IdentityServer tarafında belirtmemiz gerekli. Bunun için IdentityServer uygulaması içerisinde Config.cs  dosyasında Api ve Client için yapılandırmaları oluşturmamız gerekmektedir. Resource için GetApis veya Apis methodu içerisine aşağıdaki gibi  windowsauthapi adında yeni bir api yapılandırması oluşturalım.

public static IEnumerable<ApiResource> Apis =>
        new ApiResource[]
        {
            new ApiResource("clientcreapi", "Client Credential API"),
            new ApiResource("windowsauthapi","Windows Auth Api Resource",new[]
            {
                
                JwtClaimTypes.FamilyName,
                JwtClaimTypes.GivenName
            })
        };

Bazı durumlarda kullanıcıların ekstra bilgileri, api uygulamamız için gerekli olabilir. Eğer bu bilgilere Api tarafında ihtiyacınız varsa,  access token üzerinde api’ye gönderilecek claim tiplerini, api resource tanımlama kısmında parametre olarak belirtmemiz gerekmektedir. Bunun için Api yapılandırmasında, api içerisinde kullanacağımız ekstra claim tiplerini bir array olarak veriyoruz. Aksi takdirde access token üzerinde standart claim bilgileri bulunacaktır. Burada örnek olması için kullanıcı adı ve soyadı bilgilerini ekledim.

Client yapılandırması için GetClients (eski) veya Clients metodu içerisine aşağıdaki gibi ClientId  windowsauthclient  adında ve ClientSecrets olarak winsecret şeklinde yeni bir client yapılandırması ekliyoruz.

Config.cs

new Client
              {
                  ClientId = "windowsauthclient",
                  ClientName = "Windows Authentication Mvc Client",
                  AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
                  ClientSecrets = { new Secret("winsecret".Sha256()) },
                  EnableLocalLogin =false,
                  RefreshTokenUsage =TokenUsage.ReUse,
                  RedirectUris = { "https://localhost:44327/signin-oidc" },
                  FrontChannelLogoutUri = "https://localhost:44327/signout-oidc",
                  PostLogoutRedirectUris = { "https://localhost:44327/signout-callback-oidc" },
                  RequireConsent = false,
                  AllowOfflineAccess = true,
                  AlwaysIncludeUserClaimsInIdToken =true,
                  AllowedScopes =  new List<string>
                  {
                      IdentityServerConstants.StandardScopes.OpenId,
                      IdentityServerConstants.StandardScopes.Profile,
                      IdentityServerConstants.StandardScopes.OfflineAccess,                               
                      "windowsauthapi"
                  }

              }

Windows authentication için AllowedGrantTypes olarak, HybridAndClientCredentials şeklinde atandığını görebilirsiniz. Client uygulamamız, client secret bilgisini saklayabilen  server side bir uygulama olacağı için Hybrid Flow akışı tercih ediyorum. Eğer SPA( Single page app.)  uygulamamız için bir yapılandırma oluşturuyorsak, Grant Types Code, olarak ve RequiredPkce alanını true olarak atamamız gerekmektedir.  Yapılandırmamız içerisinde Windows Auth için önemli olan EnableLocalLogin özelliğinin false olarak atanmasıdır. Bu sayede kullanıcı, login ekranına düşmeden windows authentication ile sisteme giriş yapabilecektir. Redirect ve Logout redirect Uri kısmına client uygulamamızın adresini veriyoruz. Eğer birden fazla client uygulamamız varsa, bu alanlara birden fazla adres verebiliriz.

 

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();

    // configures IIS out-of-proc settings (see https://github.com/aspnet/AspNetCore/issues/14882)
    services.Configure<IISOptions>(iis =>
    {
        iis.AuthenticationDisplayName = "Windows";
        iis.AutomaticAuthentication = true;
    });

    // configures IIS in-proc settings
    services.Configure<IISServerOptions>(iis =>
    {
        iis.AuthenticationDisplayName = "Windows";
        iis.AutomaticAuthentication = true;
    });

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
    
    var builder = services.AddIdentityServer(options =>
        {
            options.Events.RaiseErrorEvents = true;
            options.Events.RaiseInformationEvents = true;
            options.Events.RaiseFailureEvents = true;
            options.Events.RaiseSuccessEvents = true;
        })
        .AddInMemoryIdentityResources(Config.Ids)
        .AddInMemoryApiResources(Config.Apis)
        .AddInMemoryClients(Config.Clients)
        .AddAspNetIdentity<ApplicationUser>();

    // not recommended for production - you need to store your key material somewhere secure
    builder.AddDeveloperSigningCredential();

 
}

public void Configure(IApplicationBuilder app)
{
    if (Environment.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }

    app.UseStaticFiles();

    app.UseRouting();
    app.UseIdentityServer();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

IdentityServer uygulamamızın Startup.cs  dosyasını yukarıdaki gibi yapılandırmamız gerekmektedir. IIS süreç içi ve dışı windows authentication yapılandırması, IISOptions ve IISServerOptions olarak belirtilmiştir. Uygulamamızın Sql server yapılandırmasını, appsettings.json dosyasında eklediğimiz connection string ve  Db Context sınıfımızı belirterek ConfigureServices metoduna ekliyoruz. User ve Role sınıflarımızı kullanarak uygulamanın identity  yapılandırmasını belirtiyoruz ve  InMemory olarak Identity server yapılandırmasını (Config.cs) ekliyoruz.

Ayrıca IIS’in windows authentication ve anonymous authentication kullanabilmesi için launchSettings.json dosyasının iisSettings kısmı aşağıdaki gibi olmalıdır.

launchSettings.json

"iisSettings": {
   "windowsAuthentication": true,
   "anonymousAuthentication": true,
   "iisExpress": {
     "applicationUrl": "http://localhost:5000",
     "sslPort": 44366
   }

ExternalController Dosyası Üzerinde Düzenleme

Windows auth,  identityserver  tarafında external kimliklendirme işlemi olarak ele alınmaktadır. IdentityServer template içerisinde external auth için gerekli metotlar hazır olarak gelmektedir. Fakat bu metotları kullanmak zorunda değilsiniz. Kendi akışınızı geliştirebilirsiniz.

Kısaca Windows auth için external login işlemi şu şekildedir:

  • Login isteği ReturnUrl ve provider bilgisi ile Challenge() methoduna gelir. Challenge metodu harici(external)  authentication sağlayıcıya  gidiş-gelişi başlatır. Provider bilgisi ile ProcessWindowsLoginAsync() metoduna ReturnUrl parametresi ile yönlendirilir.  ProcessWindowsLoginAsync metodu, Account klasörü altında bulunan AccountOptions metodu içerisinde tanımlanan WindowsAuthenticationSchemeName ile windows auth işlemini gerçekleştirir.
  • Daha sonra Callback() methoduna akışı yönlendirir. Callback metodu, External Cookie ile authenticate olup, external cookie’den aldığı kullanıcı (claims) bilgileri ile veri tabanında kayıtlı kullanıcı, kullanıcının claims , ve provider bilgilerini almak için  FindUserFromExternalProviderAsync() metodunu çalıştırır.
  • Eğer kullanıcı veri tabanında bulunamaz ise AutoProvisionUserAsync() methodu ile veri tabanına yeni kullanıcı oluşturulur. Bu sayede Callback metodu içerisinde kullanıcı (ApplicationUser) ve claim nesnesi doldurulur ve SignIn işlemi gerçekleştirilir.
  • Eğer istek yapan Client uygulamanın ayarlarında kullanıcı rızası veya izni (Consent) true olarak ayarlanmış ise Callback metodu Consent sayfasına returnUrl ile yönelendirecektir. Kullanıcı izin ver butonuna tıkladığında Client uygulamaya geri dönecektir(Redirect). Client yapılandırma içerisinde, RequiredConsent  false ise doğrudan Client uygulamaya yönlenecektir.

Bu şekilde external veya windows authentication işlemi gerçekleştirilir.

Sadece windows auth. için external cookie authentication scheme ile authentication işlemini yapmamız gerekiyor.  Bunun için Callback metodu içerisine HttpContext.AuthenticateAsync() metodunu aşağıdaki şekilde değiştirmeliyiz.

var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);

Api (Resource)

Client uygulamamızın erişmesi gereken bir veri kaynağı (resource) varsa, bu kısımda web api yapılandırmasını anlatmaya çalışacağım.  IdentityServer tarafında Config.cs dosyasında belirttiğimiz ApiResource kullanacağız. Aslında Client Credential örneğinde bulunan api’dan hiç bir farkı bulunmuyor.  Sadece yapılandırma kısmında (Startup.cs) JwtBearer, Audience özelliğini IdentityServer Config dosyasında verdiğimiz ApiResource name ve Authority olarak IdentityServer uygulamamızın adresini vermemiz yeterlidir.

services.AddAuthentication("Bearer")
       .AddJwtBearer("Bearer", options =>
       {
           options.Authority = "https://localhost:44366";
              
               options.RequireHttpsMetadata = true;

           options.Audience = "windowsauthapi";
       });

Startup.cs dosyasında Configure metodu içerisinde app.UseAuthentication() ve app.UseAuthorization() kısımlarını unutmamız gerekmektedir.

Ayrıca, User claim bilgilerini görebilmek için IdentityController adında yeni bir controller ekliyoruz. Bu Controller,  Authorize data annotation’a sahip olmalıdır. Bu sayede access token olmayan kullanıcı veya client bu bilgilere erişemeyecektir. Get metodu ile kullanıcının Cliam bilgilerini json olarak listeliyoruz.

[Route("api/[controller]")]
  [ApiController]
  [Authorize]
  public class IdentityController : ControllerBase
  {
      public IActionResult Get()
      {
          return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
      }
  }

Client Uygulama

Şimdi windows auth ile giriş yapıp, api ‘e istek gönderecek ve ekranda gösterecek Mvc uygulamamızı oluşturalım.

Client uygulamamız, HomeController içerisinde Index sayfası Authorize attribute sahiptir. Böylece Index sayfasına bağlanan kullanıcı giriş (login) işlemi için IdentityServer’a yönlendirilir. Windows auth ile giriş yapan kullanıcı access token ile birlikte Index sayfasına erişir.

[Authorize]
public IActionResult Index()
{
return View();
}

HomeController içerisinde CallApi sayfası bulunmaktadır. Bu metot veya sayfa ApiResource’in Identity controller’ina istekte bulunarak dönen json data’ı json.cshtml sayfasında göstermektedir.

HomeController -> CallApi

public async Task<IActionResult> CallApi()
{
    var accessToken = await HttpContext.GetTokenAsync("access_token");

    var client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    var content = await client.GetStringAsync("https://localhost:44379/api/identity");

    ViewBag.Json = JArray.Parse(content).ToString();
    return View("json");
}

json.cshtml

@{
    ViewData["Title"] = "json";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

Response -  Client Credential Grant - API :

<pre>@ViewBag.Json</pre>

 

Client uygulamamızın OpenId Connect kullanarak authenticate olabilmesi için Startup.cs dosyamız aşağıdaki gibi olmalıdır.

Startup.cs (Client App)

private const string identityServerUrl = "https://localhost:44366/";

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllersWithViews();
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });


            var requireWindowsProviderPolicy = new AuthorizationPolicyBuilder()
          .RequireClaim("http://schemas.microsoft.com/identity/claims/identityprovider", "Windows")
          .Build();

            services.AddAuthorization(options =>
            {
                options.AddPolicy(
                  "RequireWindowsProviderPolicy",
                  requireWindowsProviderPolicy
                );
            });

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
                options.DefaultSignOutScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie()
            .AddOpenIdConnect(options =>
            {
                options.SignInScheme = "Cookies";
                options.SignOutScheme = "OpenIdConnect";
                options.Authority = identityServerUrl;
                options.RequireHttpsMetadata = true;

                options.ClientId = "windowsauthclient";
                options.ClientSecret = "winsecret";
                options.ResponseType = "code id_token";
                options.GetClaimsFromUserInfoEndpoint = true;

                options.Scope.Add("windowsauthapi");
                options.Scope.Add("profile");
                options.Scope.Add("offline_access");
                options.SaveTokens = true;
                // Set the correct name claim type
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name"
                };
            });

            
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();
            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }

İlk satırda gördüğünüz IdentityServerUrl’ini, test olduğu için  const (sabit) olarak tanımladım. İsterseniz bu adresi appsettings.json yapılandırma dosyasında bir özellik olarak tanımlayıp, bu dosyadan okuyabilirsiniz.

ConfigureServices() metodu içerisinde sırasıyla CookiePolicy ve AuthorizationPolicy yapılandırmaları bulunmaktadır. Önemli kısım, AddAuthentication ve AddOpenIdConnect  yapılandırmalarıdır.

AddAuthentication yapılandırmasında DefaultScheme “Cookies” olmalıdır. Challenge ve SignOut scheme “OpenIdConnect” şeklinde atanmalıdır.

AddOpenIdConnect kısmında SignIn ve SignOut Scheme özellikleri AddAuthentication kısmı ile aynı şekilde atanmalıdır. Authority özelliğine IdentityServer uygulamamızın adresini veriyoruz. RequiredHttpsMetadata özelliği true vererek https olması gerekliliğini belirtiyoruz. Identityserver uygulamasının  Config dosyasında windows auth için daha önce  ClientId ve ClientSecret bilgilerini belirlemiştik. Belirlediğimi bu bilgileri burada ClientId ve ClientSecret özelliklerine atıyoruz. ClientCredential ve Hybrid bir akış kullandığı için response type özelliğini  “code id_token” olarak atıyoruz. GetClaimsFromUserInfoEndpoint özelliğini true olarak gönderdiğimiz zaman token üzerinde kullanıcı bilgileri de gönderilir. Daha sonra Client için belirlediğimiz scope’ları ve erişmek istediğimiz ApiResource adını scope olarak veriyoruz. Bu şekilde Client uygulamamızın OpenIdConnect yapılandırmasını tamamlıyoruz.

Configure() metodu kısmında CookiePolicy,Authentication ve Authorization kısımlarını uygulamamızın pipeline’a tanımlamayı unutmayalım.

Bu şekilde Client uygulamamızı tamamlamış oluyoruz.

 

Sonuç:

IdentityServer ile client uygulamamızı kullanan kullanıcıların, sisteme windows authentication ile login olmasını sağladık ve external auth işlemine kısaca göz atmış olduk. Ayrıca Client uygulamamızda Cookie Scheme kullanarak windows authentication işlemini gerçekleştirebiliyoruz.Örnek uygulamanın kodlarına erişmek için https://github.com/muratguven/IdentityServerSamples github adresimden erişebilirsiniz. Bu örnekte Asp.Net Core Mvc bir client olduğu için HybridAndClientCredential akışını kullandım. Spa veya javascript uygulamalar için Code akışını kullanabilirsiniz. Uygulama içerisinde WindowsAuth klasörü altında api ve client uygulamalara erişebilirsiniz. Örnek uygulamayı kullanmadan önce veri tabanı connection string’i yazmayı ve migration yapmayı unutmayın.