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

OpenIddict #5 – Refresh Token

Merhaba,

Bu içeriğimizde OpenIddict kütüphanesinde refresh token kullanımını nasıl etkinleştirebileceğimizi inceliyor olacağız.

Refresh Token Neydi?

Malumunuz refresh token, OAuth 2.0 tabanlı bir yetkilendirme mekanizmasıdır. Access token’ın geçerlilik süresi sona erdiği taktirde, kullanıcıyı login ekranına yönlendirmeye gerek kalmaksızın, yani kullanıcının yaptığı işin odağını bozmaksızın yeniden bir access token alabilmesini sağlayan bir davranışa sahiptir. Yapısal olarak access token’dan daha uzun ömürlü olan refresh token’lar uzun süreli oturumları yönetmek için yaygın olarak kullanılmaktadırlar.

Refresh Token’ı Etkinleştirme

Uygulamanızda OpenIddict ile refresh token’ı etkinleştirebilmek için Authorization Server’da AllowRefreshTokenFlow fonksiyonu eşliğinde aşağıdaki gibi refresh token akışını yapılandırmanız gerekmektedir.

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore;
using OpenIddict.RefreshToken.Example.AuthorizationServer.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => options.LoginPath = "/account/login");

builder.Services.AddOpenIddict()
    .AddCore(options => options.UseEntityFrameworkCore()
                               .UseDbContext<ApplicationDbContext>())
    .AddServer(options =>
    {
        options.SetTokenEndpointUris("/connect/token")
               .
               .
               .

               .AllowClientCredentialsFlow()
               .AllowAuthorizationCodeFlow()
               .AllowRefreshTokenFlow()

               .
               .
               .

        options.RegisterScopes("read", "write");
    });

.
.
.
app.Run();

Devamında ise yine Authorization Server’daki ‘AuthorizationController’ içerisindeki Exchange fonksiyonunda aşağıdaki gibi refresh token operasyonlarını gerçekleştiriniz.

    public class AuthorizationController : Controller
    {
        .
        .
        .

        [HttpPost("~/connect/token")]
        public async Task<IActionResult> Exchange()
        {
            var request = HttpContext.GetOpenIddictServerRequest();
            ClaimsPrincipal principal = null;
            if (request?.IsAuthorizationCodeFlow() is not null)
            {
                .
                .
                .
            }
            else if (request?.IsRefreshTokenGrantType() is not null)
            {
                principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;

                principal.AddClaim(Claims.Name, "Gençay");
                principal.AddClaim(Claims.Profile, "Gençay Profile");
                principal.AddClaim(Claims.Email, "Gençay Email");
                principal.AddClaim("ornek-claim", "Örnek Claim");
                principal.AddClaim(JwtRegisteredClaimNames.Aud, "Example-OpenIddict");

                foreach (var claim in principal.Claims)
                    claim.SetDestinations(Destinations.AccessToken, Destinations.IdentityToken);

                //Principal'ı yani kullanıcıyı doğrula
                //if ((await _userManager.GetUserAsnyc(principal)) != null)
                //{

                //}
            }
            else if (request?.IsClientCredentialsGrantType() is not null)
            {
                .
                .
                .
            }
            else
                throw new NotImplementedException("The specified grant type is not implemented.");
            return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }
        .
        .
        .
    }

Authorization Server’da refresh token yapılandırmasını gerçekleştirdikten sonra gerisi artık client tarafındaki çalışmalara kalmaktadır. Bizler burada izafi olarak refresh token’ı kullanmayı ele alacak ve öylece içeriğimizi sonlandırıyor olacağız.

Misal olarak, client uygulamasının ‘Index.cshtml’ dosyasına aşağıdaki gibi refresh token talebinde bulunabileceğimiz bir adres eklememiz ve ‘AuthenticationController’ içerisinde gerekli çalışmaları yapmamız refresh token’ı uygulama çapında test edebilmek için gayet yerinde olacaktır.

-- Index.cshtml

@model string

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<form asp-action="Index" asp-controller="Home" asp-route-button="GET">
    <button>Get İstek Yap</button>
</form>

<form asp-action="Index" asp-controller="Home" asp-route-button="POST">
    <button>Post İstek Yap</button>
</form>

<form asp-action="Logout" asp-controller="Authentication" method="post">
    <button class="btn btn-lg btn-danger" type="submit">Sign out</button>
</form>

<a asp-action="Refresh" asp-controller="Authentication">Refresh Token</a>

@if (ViewBag.Properties is not null)
{
    <ul>
        @foreach (var property in ViewBag.Properties as IOrderedEnumerable<KeyValuePair<string, string>>)
        {
            <li>@property.Key ====> @property.Value</li>
        }
    </ul>
}

<p>
    @Model
</p>
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using OpenIddict.Client.AspNetCore;
using System.Security.Claims;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace OpenIddict.RefreshToken.Example.Client1.Controllers
{
    public class AuthenticationController : Controller
    {
        [HttpGet("~/login")]
        public IActionResult LogIn(string returnUrl)
        {
            var properties = new AuthenticationProperties(new Dictionary<string, string?>
            {
                [OpenIddictClientAspNetCoreConstants.Properties.Issuer] = "https://localhost:7249"
            });
            properties.RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/";

            //Challenge metodu ile OpenIddict middleware'ı sayesinde ilgili Issuer'a karşılık gelen client bilgilerini authorization server'a yönlendiriyoruz.
            return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
        }

        [HttpPost("~/logout"), ValidateAntiForgeryToken]
        public async Task<ActionResult> LogOut(string returnUrl)
        {
            //Elde bulunan authentication cookie bilgilerini elde ediyoruz. Eğer yoksa zaten kullanıcının henüz oturum açmadığını anlıyoruz.
            var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            if (result is not { Succeeded: true })
                return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/");

            //SignOut yaparak mevcut authentication cookie bilgilerini temizliyoruz.
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

            var properties = new AuthenticationProperties(new Dictionary<string, string?>
            {
                [OpenIddictClientAspNetCoreConstants.Properties.Issuer] = "https://localhost:7249",
                [OpenIddictClientAspNetCoreConstants.Properties.IdentityTokenHint] = result.Properties.GetTokenValue(OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken)
            });
            properties.RedirectUri = Url.IsLocalUrl(returnUrl) ? returnUrl : "/";

            return SignOut(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
        }

        [HttpGet("~/callback/login/{provider}"), HttpPost("~/callback/login/{provider}"), IgnoreAntiforgeryToken]
        public async Task<ActionResult> LogInCallback()
        {
            // OpenIddict tarafından doğrulanan yetkilendirme verilerini elde ediyoruz.
            var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);

            if (result.Principal.Identity is not ClaimsIdentity { IsAuthenticated: true })
                throw new InvalidOperationException("The external authorization data cannot be used for authentication.");

            var claims = new List<Claim>(result.Principal.Claims
                             .Select(claim => claim switch
                             {
                                 { Type: Claims.Subject } => new Claim(ClaimTypes.NameIdentifier, claim.Value, claim.ValueType, claim.Issuer),
                                 { Type: Claims.Name } => new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),
                                 _ => claim
                             }));

            var identity = new ClaimsIdentity(claims,
                authenticationType: CookieAuthenticationDefaults.AuthenticationScheme,
                nameType: ClaimTypes.Name,
                roleType: ClaimTypes.Role);

            var properties = new AuthenticationProperties(result.Properties.Items);

            //Gerekirse authorization server tarafından döndürülen tokenlar authentication cookie'de de saklanabilir.
            properties.StoreTokens(result.Properties.GetTokens().Where(token => token switch
            {
                {
                    Name: OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or
                          OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken or
                          OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken
                } => true,
                _ => false
            }));

            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), properties);

            return Redirect(properties.RedirectUri);
        }

        [HttpGet("~/callback/logout/{provider}"), HttpPost("~/callback/logout/{provider}"), IgnoreAntiforgeryToken]
        public async Task<ActionResult> LogOutCallback()
        {
            var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
            return Redirect(result!.Properties!.RedirectUri);
        }

        [HttpGet("~/refresh")]
        public async Task<IActionResult> Refresh()
        {
            string refreshToken = await HttpContext.GetTokenAsync(OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken);
            HttpClient httpClient = new HttpClient();
            RefreshTokenRequest refreshTokenRequest = new RefreshTokenRequest()
            {
                ClientId = "my-client1",
                ClientSecret = "my-client-secret1",
                RefreshToken = refreshToken,
                Address = (await httpClient.GetDiscoveryDocumentAsync("https://localhost:7249")).TokenEndpoint
            };
            TokenResponse tokenResponse = await httpClient.RequestRefreshTokenAsync(refreshTokenRequest);
            AuthenticationProperties properties = (await HttpContext.AuthenticateAsync()).Properties;

            properties.StoreTokens(
                new List<AuthenticationToken> {
              new AuthenticationToken
                                     {
                                         Name = OpenIddictClientAspNetCoreConstants.Tokens.BackchannelIdentityToken,
                                         Value = tokenResponse.IdentityToken
                                     },
              new AuthenticationToken
                                     {
                                         Name = OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken,
                                         Value = tokenResponse.AccessToken
                                     },
              new AuthenticationToken
                                     {
                                         Name = OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken,
                                         Value = tokenResponse.RefreshToken
                                     },
              new AuthenticationToken
                                     {
                                         Name = OpenIdConnectParameterNames.ExpiresIn,
                                         Value = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn).ToString("O")
                                     },
                                       });
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, (await HttpContext.AuthenticateAsync()).Principal, properties);
            return RedirectToAction("Index", "Home");
        }
    }
}

Burada 97 ile 136. satır aralığına göz atarsanız eğer resfresh token ile ilgili gerekli çalışmalar yürütülmektedir. Önce 99. satırda uygulamadaki refresh token elde edilmekte ve ardından Authorization Server’a bu refresh token eşliğinde yapılan request neticesinde yeni bilgiler edinilmektedir. Burada dikkat ederseniz RefreshTokenRequest, TokenResponse vs. gibi sınıfları kullanabilmek yani bu operasyonu sağlıklı bir şekilde yürütebilmek için IdentityModel kütüphanesinden istifade etmekteyiz. Velhasıl, 111 ile 133. satır aralığında ise gelen access token, identity token ve refresh token gibi yeni bilgiler uygulama bazında güncellenmekte ve 134. satırda SignInAsync metodu ile güncel verilerle tekrardan oturum tazelenmektedir.

Tabi burada client uygulamasının konfigürasyonlarında da refresh token’ın aşağıdaki gibi AllowRefreshTokenFlow metodu eşliğinde aktifleştirilmesi ve offline_access scope’u eşliğinde refresh token değerinin auth server’dan talep edilmesi gerekmektedir.

.
.
.
builder.Services.AddOpenIddict()
                .AddCore(options =>
                {
                    options.UseEntityFrameworkCore()
                           .UseDbContext<ApplicationDbContext>();
                })
                .AddClient(options =>
                {
                    options.SetRedirectionEndpointUris("/callback/login/local")
                           .SetPostLogoutRedirectionEndpointUris("/callback/logout/local")

                           .AddDevelopmentEncryptionCertificate()
                           .AddDevelopmentSigningCertificate()

                           .AllowAuthorizationCodeFlow()
                           .AllowRefreshTokenFlow()

                           .UseAspNetCore()
                            .EnableStatusCodePagesIntegration()
                            .EnableRedirectionEndpointPassthrough()
                            .EnablePostLogoutRedirectionEndpointPassthrough();

                    options.UseSystemNetHttp();

                    options.AddRegistration(new OpenIddictClientRegistration
                    {
                        Issuer = new Uri("https://localhost:7249", UriKind.Absolute),

                        ClientId = "my-client1",
                        ClientSecret = "my-client-secret1",
                        Scopes = { "read", "write", "offline_access" },

                        RedirectUri = new Uri("https://localhost:7247/callback/login/local", UriKind.Absolute),
                        PostLogoutRedirectUri = new Uri("https://localhost:7247/callback/logout/local", UriKind.Absolute)
                    }); ;
                })
                .AddValidation(options =>
                {
                    options.UseLocalServer();
                    options.UseAspNetCore();
                });

.
.
app.Run();

İşte bu kadar… 19. satırdaki yapılandırmanın yanında 34. satırdaki offline_access scope’u eşliğinde refresh token talebi gerçekleştirilerek, artık uygulamanızda refresh token’ı kullanabilir ve kullanıcı aktifken biryandan da oturum sürecini güncelleyebilirsiniz. Tabi bunun için refresh token iznine sahip yeni bir client oluşturulması gerekecektir. Ee haliyle Authorization Server uygulamasındaki ‘ClientsController’ içerisindeki ‘CreateClient’ metodunu aşağıdaki gibi güncelleyebilir ve ardından bu ayarlarda arayüz üzerinden yeni bir client uygulaması oluşturabilirsiniz.

    public class ClientsController : Controller
    {
        .
        .
        .
        [HttpPost]
        public async Task<IActionResult> CreateClient(ClientCreateVM model)
        {
            if (ModelState.IsValid)
            {
                var client = await _openIddictApplicationManager.FindByClientIdAsync(model.ClientId);
                if (client is null)
                {
                    await _openIddictApplicationManager.CreateAsync(new OpenIddictApplicationDescriptor
                    {
                        ClientId = model.ClientId,
                        ClientSecret = model.ClientSecret,
                        DisplayName = model.DisplayName,
                        RedirectUris = { new(model.RedirectUrl) },
                        PostLogoutRedirectUris = { new(model.PostLogoutRedirectUri) },
                        Permissions = {
                                        OpenIddictConstants.Permissions.Endpoints.Token,
                                        OpenIddictConstants.Permissions.Endpoints.Authorization,
                                        OpenIddictConstants.Permissions.Endpoints.Logout,

                                        OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
                                        OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
                                        OpenIddictConstants.Permissions.GrantTypes.RefreshToken,

                                        OpenIddictConstants.Permissions.Prefixes.Scope + "read",
                                        OpenIddictConstants.Permissions.Prefixes.Scope + "write",

                                        Permissions.Scopes.Email,
                                        Permissions.Scopes.Profile,
                                        Permissions.Scopes.Roles,

                                        OpenIddictConstants.Permissions.ResponseTypes.Code
                                      }
                    });
                    ViewBag.Message = "Client başarıyla oluşturulmuştur.";
                }
                else
                    ViewBag.Message = "Client zaten mevcuttur.";
                return View();
            }

            ViewBag.Message = "Lütfen client bilgilerini tam giriniz.";
            return View(model);
        }
    }

28. satıra göz atarsanız oluşturulacak client’ta refresh token yetkisi verilmektedir.

Tüm bu çalışmalardan sonra artık uygulamayı ayağa kaldırıp test edebilirsiniz.OpenIddict #5 - Refresh TokenYukarıdaki ekran görüntüsünü incelerseniz eğer refresh token’ın başarıyla çalıştığını gözlemleyeceksiniz.

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

Not : Örnek çalışmayı aşağıdaki github adresinden edinebilirsiniz.
https://github.com/gncyyldz/OpenIddict.RefreshToken.Example

Bunlar da hoşunuza gidebilir...

2 Cevaplar

  1. Gökhan dedi ki:

    hocam merhaba,

    signalr ile ilgili ilk uygulama videonuzdaki kodlarla localde sorunsuz çalıştırabiliyorum. (https://www.youtube.com/watch?v=hIW3wt3tvmc&list=PLQVXoXFVVtp3RSycdru4WpnfPEOFxONiX&index=2). aynı zamanda bunu localde değilde webhost’uma yükleyerek yapmak istiyorum. uygulamanızdaki server kısmını yüklediğim zaman masaüstümde çalıştırdığım client bağlanmıyor. alan adım https://www.srocastle.com https://localhost:5001 yazan yerleri kendi alan adımla güncelledim. sabaha kadar araştırdım türlü denemeler yaptım çözümünü bulamadım. sizden dileğim bu en temel halinin server kısmını localhosttan değilde başka bir hosta yükleyip anlatmanız olabilir mi 🙂

    localde direk masaüstümde client html dosyasını direk çalıştırdığımda aldığım hata:

    “Access to fetch at ‘https://srocastle.com/myhub/negotiate?negotiateVersion=1’ from origin ‘null’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.”

    aynı html hostuma yükledim onda aldığım verdiği hata:

    “HubConnection.ts:108 POST https://srocastle.com/myhub/negotiate?negotiateVersion=1 404″ şeklinde.

    • Ömer dedi ki:

      Cors ayarlarına bakmanızı öneririm.Eğer program.cs’de izin veriyorsan sunucudaki web.config dosyasına bak belki orada engellemiş olabilirsin.Daha önce benim de başıma aynı olay geldi.Web.config dosyasına sadece htpp isteklerine izin veriliyormuş hata burdan kaynaklıymış.

Bir yanıt yazın

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