Temel Kimlik Doğrulama (Basic Authentication), OAuth2 ve Token (Belirteç) Tabanlı Kimlik Doğrulama Yaklaşımları

Temel Kimlik Doğrulama (Basic Authentication), OAuth2 ve Token (Belirteç) Tabanlı Kimlik Doğrulama Yaklaşımları

1. Temel Kimlik Doğrulama

Basic Authentication konusunu anlamadan önce SpringSecurity hiyerarşiside, bunu kapsayan UsernamePassword Authentication konusuna değinmek doğru olacaktır.

1.1 Kullanıcı Adı ve Parola ile Kimlik Doğrulama (Username & Password Authentication)

En basit ve anlaşılır doğrulama yöntemlerinden bir tanesi olan Username&Password Authentication, kullanıcıdan gelen isteklerdeki kullanıcı adı ve şifre bilgilerini kullanarak doğrulama işlemini gerçekleştirir.

SpringSecurity’de Username&Password Authentication, UsernamePasswordAuthenticationFilter sınıfı tarafından gerçekleştirilir. [1] Bu filtre, kullanıcıdan gelen isteklerdeki kullanıcı adı ve şifre bilgilerini kullanarak UsernamePasswordAuthenticationToken sınıfı nesnesi oluşturur. Bu nesne, AuthenticationManager sınıfına gönderilir.

  • AuthenticationManager sınıfı, AuthenticationProvider sınıfı nesnesi ile kullanıcı adı ve şifre bilgilerini karşılaştırır. Eğer kullanıcı adı ve şifre bilgileri doğru ise Authentication sınıfı nesnesi oluşturur ve bu nesneyi SecurityContextHolder sınıfına ekler.

UsernamePasswordAuthenticationFilter sınıfı, AbstractAuthenticationProcessingFilter sınıfından türediği için temel kimlik doğrulama akışını (attemptAuthentication, successfulAuthentication, unsuccessfulAuthentication) bu üst sınıfın sunduğu halihazırdaki yöntemlerle yürütür.[3] Varsayılan olarak yalnızca POST (Post method - Veri Gönderme Yöntemi) isteklerine izin verir ve form parametrelerini “username” ile “password” olarak kabul eder; ancak ihtiyaca göre setRequiresAuthenticationRequestMatcher metodu ile farklı bir URL (Uniform Resource Locator - Tekdüzen Kaynak Konumlayıcı) veya HTTP (Hypertext Transfer Protocol - Üstmetin Aktarım Protokolü) metodu, setUsernameParameter ve setPasswordParameter metodlarıyla da farklı parametre isimleri tanımlamak mümkündür.

Filtre zincirindeki konumu da güvenlik akışının mantığını korumak açısından önemlidir [2]: HTTP isteği önce kanal güvenliğini (ChannelProcessingFilter), oturum yönetimini (ConcurrentSessionFilter), giriş ve çıkış işlemlerini (LogoutFilter) gibi aşamalardan geçirir, ardından form tabanlı kimlik doğrulamayı UsernamePasswordAuthenticationFilter ile gerçekleştirir. Bu sayede kimlik doğrulama mantığı, temel güvenlik işlemlerinden sonra ve yetkilendirme kontrollerinden önce çalışır; hatalı bir giriş denemesi durumunda uygun hata yanıtları verilir, başarılı girişte ise sonraki filtrelerin kullanımına izin verecek şekilde güvenlik bağlamı hazırlanır.

Spring Security (Güvenlik) dahilinde iki yaklaşım bulunmaktadır. Bunlar; A. FormLogin B. Basic Authentication.

a. FormLogin

Bu yaklaşımda kullanıcıdan alınan bilgiler bir HTML form kullanılarak taşınır.

  • Bu formun action’ı SpringSecurity tarafından sağlanan bir endpoint olmalıdır.

  • Bu endpoint’e gelen istekler UsernamePasswordAuthenticationFilter sınıfı tarafından yakalanır ve kullanıcı adı ve şifre bilgileri UsernamePasswordAuthenticationToken sınıfı nesnesi oluşturulur.

  • Bu nesne, AuthenticationManager sınıfına gönderilir. AuthenticationManager sınıfı, AuthenticationProvider sınıfı nesnesi ile kullanıcı adı ve şifre bilgilerini karşılaştırır. [1]

  • Eğer kullanıcı adı ve şifre bilgileri doğru ise Authentication sınıfı nesnesi oluşturur ve bu nesneyi SecurityContextHolder sınıfına ekler.

Sisteme bir istek yapıldığında, bu istek doğrulanmamış bir kullanıcı tarafından yapılmış ise, istek reddedilir ve kullanıcı bir login sayfasına yönlendirilir.[4]

  • Bu login sayfası, SpringSecurity tarafından sağlanan bir login sayfası olabileceği gibi [2], uygulama tarafından özelleştirilmiş bir login sayfası da olabilir.

  • Kullanıcı, login sayfasında kullanıcı adı ve şifre bilgilerini girerek sisteme giriş yapar ve doğrulama işlemi gerçekleştirilir.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@EnableWebSecurity
public class SecurityConfig {

 @Bean
 public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  return http
      .authorizeHttpRequests((authorizeRequests) ->
          authorizeRequests
              .requestMatchers("/login").permitAll() //(1)
              .requestMatchers("/**").authenticated() //(2)
      )
      .formLogin(AbstractAuthenticationFilterConfigurer::permitAll)//(3)
      .build();
 }
} [2]
  1. /login endpoint’i herkese açık olmalıdır. Bu endpoint’e gelen istekler UsernamePasswordAuthenticationFilter sınıfı tarafından yakalanır ve kullanıcı adı ve şifre bilgileri UsernamePasswordAuthenticationToken sınıfı nesnesi oluşturulur. Bu nesne, AuthenticationManager sınıfına gönderilir. AuthenticationManager sınıfı, AuthenticationProvider sınıfı nesnesi ile kullanıcı adı ve şifre bilgilerini karşılaştırır. Eğer kullanıcı adı ve şifre bilgileri doğru ise Authentication sınıfı nesnesi oluşturur ve bu nesneyi SecurityContextHolder sınıfına ekler.

  2. Diğer tüm endpoint’lerin erişimi sınırlandırılmıştır. Bu isteklerin doğrulanması gerekir. Doğrulama işlemi UsernamePasswordAuthenticationFilter sınıfı tarafından gerçekleştirilir. Doğrulama yapılamaz ise kullanıcı login sayfasına yönlendirilir.

  3. Gerekli doğrulama formLogin ile sağlanır. Bu sayede SpringSecurity tarafından sağlanan login sayfası kullanılır.

1.2 Basic Authentication Nedir?

Basic veya HTTP Authentication, FormLogin yönteminden farkı kullanıcı bilgilerinin taşınma şeklidir. Bu yöntemde kullanıcı bilgileri, HTTP isteğinin header(başlık) kısmında taşınır. [1], [5] Bu bilgiler, Base64 ile kodlanmış şekilde taşınır.

Bir kullanıcı sisteme bir istek gönderdiğinde header kısmında basic authenticaition ile ilgili bilgiyi eklemelidir. Bu bilgi, kullanıcı adı ve şifre bilgilerinin Base64 ile kodlanmış halidir.

  • Bu bilginin formatı “Basic base64(username:password)” şeklindedir.

İstek sunucuya ulaştığında, sunucu bu bilgiyi alır ve kullanıcı adı ve şifre bilgilerini çözümleyerek doğrulama işlemini gerçekleştirir.

Doğrulama yapılamaz ise 401 Unauthorized hatası döner.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableWebSecurity
public class SecurityConfig {

 @Autowired
 private BasicAuthenticationEntryPoint authenticationEntryPoint;

 @Bean
 public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  return http
      .authorizeHttpRequests(authorizeHttpRequests ->
          authorizeHttpRequests
              .anyRequest().authenticated() //(1)
      )
      .httpBasic(
          httpBasic -> httpBasic //(2)
              .authenticationEntryPoint(authenticationEntryPoint)
      )
      .build();
 }
}
  1. Tüm isteklerin erişimi sınırlandırılmıştır. Bu isteklerin doğrulanması gerekir. Doğrulama işlemi BasicAuthenticationFilter sınıfı tarafından gerçekleştirilir. Doğrulama yapılamaz ise 401 Unauthorized hatası döner.

  2. Gerekli doğrulama basic authentication ile sağlanır.

Basic Authentication içerisindeki adımlar, stateless (durum bilgisi tutulmayan) bir yaklaşım olması dolayısıyla, sisteme gelen her bir istekte tekrarlanır.[4], [5]

Örnek bir header;

Authorization: Basic dGVzdDoxMjPCow==

  • Basic Authentication yaklaşımında bilgiler kolayda decode edilebilir bir formattadır. Bu nedenle güvenilir bağlantılar üzerinden kullanılmalıdır.

  • Basic Authentication yaklaşımı, stateless bir yaklaşımdır. Bu nedenle her istekte tekrarlanır. Bu durum, performansı olumsuz etkileyebilir. Stateful (durum bilgisi tutulan) alternatiflerin perfomans konusunda etkilerine kendi başlıklarında değineceğiz.

  • Stateless oluşu logout (oturumu kapatma) işlemini zorlaştırır. Logout işlemi için, client (istemci) tarafında tutulan bilgilerin silinmesi gerekir. Bu durum, client tarafında uygulanması gereken ekstra bir işlemdir.
    • Tarayıcı tabanlı uygulamalarda token’lar genellikle localStorage, sessionStorage veya HTTP-only çerezlerde saklanır. [5] Kullanıcı çıkış yaptığında, bu depolama alanlarını kontrol edip, eğer token bir çerezse aynı isim ve path (erişim yolu) ile, geçmiş bir tarihe ayarlanmış Set-Cookie yanıtı göndererek silinmesini sağlamak gerekir. Eğer localStorage veya sessionStorage kullanıyorsanız, uygulama kodunuzda localStorage.removeItem(“access_token”), sessionStorage.clear() gibi çağrılarla bu verilerin tamamen bellekten atıldığından emin olmalısınız. Mobil veya masaüstü istemcilerde ise benzer şekilde uygulamanın güvenli depolama katmanından (Keychain, Keystore vs.) tüm kimlik bilgilerini kaldıran bir logout metodu tanımlamak gerekir; aksi takdirde uygulama hâlâ eski token’larla konuşmaya devam edebilir. [5]
  • Bazı durumlarda, ilerleyen isteklerde kullanılacak token bilgisinin üretilmesi için tetikleyici olarak kullanılır.

2. OAuth 2.0

  • OAuth 2.0, genellikle yetkilendirme için kullanılan kullanıcı kimlik bilgilerini paylaşmadan kaynaklara güvenli ve kontrollü erişim sağlayan bir kimlik doğrulama erişim yetkilendirme standardıdır. Belirlenen bu standartlara göre hareket eden bir sistem, kullanıcıların üçüncü taraf uygulamalara erişim izni vermesine olanak tanır.

  • API’ların (Application Programming Interface – Uygulama Programlama Arayüzü) güvenliğini sağlamak ve üçüncü taraf uygulamaların kullanıcı verilerine kullanıcının izniyle erişmesini sağlamak için yaygın olarak benimsenmiştir.

  • Spring Security kapsamlı bir OAuth 2.0 desteği sağlar. [1]

2.1 OAuth2 Terimleri

  • Resource Owner : Korunan kaynağa erişebilen varlık. Genellikle bu, son kullanıcı veya bir hizmet olur.

  • Resource Server : Korunan kaynakları barındıran sunucu. Bu, client’in erişim istediği sunucudur.

  • Client : Korunan kaynaklara, resource owner(kaynak sahibi) adına, erişim talep eden uygulama.

  • Authorization Server : Resource owner’ın kimliğini doğrulayan ve ilgili yetkilendirmeyi aldıktan sonra access tokenları(erişim belirteçleri) ilişkilendiren sunucu. Bu yetkilendirme içerisinde, yetkinin var olup olmadığı ile beraber hangi kapsamda olduğu bilgisi de yönetilir. Erişim istenen kapsamlar kullanıcı onayından sonra access token ile ilişkilendirilir ve bu token ile izin verilen kapsamda korunan kaynaklara erişim sağlanır.

  • Access Token : Client’in resource owner adına korunan kaynaklara erişim sağlamak için kullandığı bir token.

  • Scope: İstemcinin resource owner’dan talep ettiği izinlerin bir listesi.

  • Redirect URI : Erişimi onayladıktan veya reddettikten sonra resource owner’ın yönlendirileceği URI.

  • Authorization Code : Client’in bir access token talep ederken kullanacağı kısa süreli token.

OAuth2, katı bir yapıdan ziyade daha çok bir paradigmadır. Birbirinden farklı ortamlar ve uygulamaların önceden tanımlanmış kurallara göre özgün OAuth2 yapılandırmaları bulunur. Bu kurallar kümesi, bir kullanıcıyı doğrulamak ve üçüncü taraf bir uygulamaya erişim izni vermek için akış oluşturur.

2.2 OAuth2 Akışları

OAuth2’de birden fazla akış bulunmaktadır, en yaygın olanları şunlardır;

a. Authorization Code Flow (Yetkilendirme Kodu Akışı)

OAuth2’de en yaygın kullanılan akıştır. Client’in önemli bilgilerini güvenli bir şekilde saklaması için idealdir. Örneğin, bir uygulamadaki yetkilendirmenin başka bir uygulama aracılığı ile sağlandığı durumlarda kullanılır.

Şekil 1’de görüldüğü gibi kullanıcı uygulamaya erişim izni verir, uygulama Authorization Server’dan yetkilendirme kodu alır, bu kodla Access (ve Refresh) Token edinir; son olarak Access Token’ı kullanarak Resource Server’dan korunan veriyi çeker.


Şekil 1. Authorization Code Flow (Yetkilendirme Kodu Akışı)


b. Client Credentials Flow (İstemci Kimlik Bilgisi Akışı)

Client’in resource owner olduğu ve client’in kendi kaynaklarına erişim talep ettiği durumlarda kullanılan akıştır. Örneğin uygulama ayağa kalkarken uygulama konfigürasyonu için gerekli bilgileri başka bir veri kaynağında tuttuğumuzda ve bu kaynak için bir yetkilendirme gerekli olduğu durumda uygulamamızı bu akış sayesinde yetkilendirebiliriz.

Şekil 2’de stemci, kendi kimlik bilgileriyle Authorization Server’dan doğrudan Access Token talep eder; Authorization Server token’ı sağlar; ardından istemci bu Access Token ile Resource Server’dan korunan veriyi alır.

Şekil 2.Client Credentials Flow (İstemci Kimlik Bilgisi Akışı))


c. Refresh Token Flow (Yenileme Belirteci Akışı)

Bu flow, mevcut access token süresi dolmuşsa yeni bir access token almak için kullanılır. Client’e hali hazırda access ve refresh token verildiyse kullanılır. Access token süresi dolduktan sonra, client refresh token(istemci yenileme belirteci) kullanarak yeni bir access token alabilir ve tüm authorization sürecini tekrarlamak zorunda kalmaz. Burada kullanılan refresh token genellikle access token’dan çok daha uzun bir geçerlilik süresine sahiptir ve istemcinin kullanıcı müdahalesi olmadan uzun süreli erişim sürdürmesine olanak tanır. Sunucu tarafında refresh token’lar, hangi istemcinin hangi kapsamlarda (scope) ve ne kadar süreyle yeniden token alabileceğini de belirleyen kurallarla birlikte verilir; bu sayede güvenli bir token yenileme ve gerekirse iptal (revocation) mekanizması uygulanabilir.

Şekil 3’te, istemci yetkilendirme koduyla aldığı Access ve Refresh Token’ı kullanır; Access Token süresi dolunca Refresh Token ile yeni token çifti alarak veri isteklerine kesintisiz devam eder.

Şekil 3.Refresh Token Flow (Yenileme Belirteci Akışı)


Bu tarz başka akış şemaları da bulunmaktadır. Örneğin;

  • Client Initiated Backchannel Authentication Flow (CIBA),
  • Hybrid Flow,
  • JWT Bearer Flow,
  • Device Code Flow.

2.3 Spring Security OAuth 2.0

Amaç: Spring Security OAuth 2.0, Spring Security’nin bir uzantısıdır. OAuth 2.0 protokolü için kapsamlı bir destek sağlar. Rolü: OAuth 2.0 tabanlı kimlik doğrulama ve yetkilendirme mekanizmalarının Spring tabanlı uygulamalara entegrasyonunu kolaylaştırır.

Spring Security’nin OAuth 2.0 desteği iki temel özellikten oluşmaktadır:

  • OAuth2 Resource Server
  • OAuth2 Client

Spring Security’nin sunucu ve istemci tarafındaki rollerini ayırmak gerekirse, Resource Server; gelen HTTP isteklerindeki Bearer token’ları doğrulayarak korunan API uç noktalarına erişimi denetler, Client ise OAuth 2.0 yetkilendirme akışları (authorization code, client credentials, refresh token vb.) aracılığıyla erişim belirteci alır ve bu belirteci uzak servislere yaptığı çağrılarda kullanır. Token üretme, imzalama ve dağıtım işlevini üstlenen Authorization Server rolü ise Spring Security’nin çekirdek özellikleri arasında bulunmaz; bu görev, OAuth 2.0 sunucu desteğini sağlayan ayrı bir proje olan Spring Authorization Server ile yerine getirilir.

OAuth2’deki resource server, client, authorization server kullanım durumlarına göre birden fazla olabilir:

  • Çoklu Client: Dağıtık bir uygulamada birden fazla servis talepte bulunabilir.
  • Çoklu Resource Server: Bir servisin ihtiyacı olan veri birden fazla resource-server’da bulunabilir.
  • Çoklu Authorization Server: Bir servis yetkilendirilirken birden fazla yetkilendirme yöntemi sunulabilir.

Örneğin, bir OAuth2 tabanlı mikroservis mimarisi kullanıcıya dönük tek bir client uygulamasından, REST API’ları sağlayan birkaç backend ucuna sahip kaynak sunucusundan ve kullanıcıların kimlik doğrulamasını yönetmek için üçüncü taraf authorization server’dan oluşabilir.

Bu rollerden yalnızca birini temsil eden tek bir uygulamanın diğer rolleri sağlayan bir veya daha fazla üçüncü tarafla entegre olması da yaygın olan bir durumdur.

a. JWT Desteği

Spring Boot yapılandırma özelliklerini kullanarak bir JwtDecoder Bean’i yapılandıralım:

1
2
3
4
5
6
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://my-auth-server.com

Burada “issuer-uri”, token provider’ı yani Authorization Server’ı temsil eder. Bu yapılandırma aşağıdaki Java kodu ile eşdeğerdir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((authorize) -> authorize
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer((oauth2) -> oauth2
                        .jwt(Customizer.withDefaults())
                );
        return http.build();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return JwtDecoders.fromIssuerLocation("https://my-auth-server.com");
    }
}

JwtDecoder imzaları doğrulamak ve token’leri decode etmek için kullanılır.

b. Opaque Token(Opak Belirteç) Desteği

Opaque token’lar, içinde herhangi bir anlamlı bilgi taşımayan, tekil bir kimlik doğrulama belirtecidir. Yani bir resource server bu token’ı aldığı zaman kendi başına “bu kullanıcı kimdir, hangi izinlere sahip” gibi sorulara cevap veremez; bunun yerine yetkilendirme sunucusundaki bir token introspection (belirteç inceleme) endpoint’ine (uç noktası) başvurur. Böylece:

  • İçerik gizliliği: Token’ın içinde kullanıcı bilgisi veya izin detayları olmadığı için, ele geçirilse bile saldırgan doğrudan token’dan veri okuyamaz.

  • Merkezi denetim: Resource server’lar her token kontrolünü yetkilendirme sunucusuna yönlendirdiği için; token iptal edildiğinde veya kapsamı değiştirildiğinde tüm sistem anında güncel duruma geçer.

  • Kısa ömür ve cache katmanı: Introspection yanıtı genellikle kısa bir süre (örneğin birkaç saniye) önbelleğe alınarak performans artırılır, ama nihayetinde her önbellek süresi dolduğunda yeniden yetkilendirme sunucusuna gidilerek güncel yetkiler doğrulanır.Spring Boot yapılandırma özelliklerini kullanarak bir OpaqueTokenIntrospector’i yapılandıralım:

Token güvenliği sağlamak içinse şu uygulamalar önemlidir:

  • TLS(Transport Layer Security – Taşıma Katmanı Güvenliği) ile koruma: Hem client–authorization server hem de resource server–authorization server arasındaki tüm trafiğin mutlaka HTTPS üzerinden yapılması.

  • Client kimlik doğrulaması: Introspection endpoint’ine yalnızca yetkili resource server’ların erişebilmesi için client kimlik bilgileri (örneğin mTLS sertifikası veya client secret) kullanmak.

  • Token ömrü ve yenileme: Access token’ların ömrünü kısa tutup, gerektiğinde refresh token ile yenilemek; refresh token’ı ise sıkı güvenlik kontrolleriyle saklamak.

  • Rate limiting (oran sınırlama) ve logging (kayıt tutma)g: Introspection çağrılarını sınırlayıp, tüm erişim denemelerini kaydederek olağan dışı kullanım ([brute force ve replay (yeniden oynatma)]) tespitine izin vermek.

1
2
3
4
5
6
7
8
spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: https://my-auth-server.com/oauth2/introspect
          client-id: my-client-id
          client-secret: my-client-secret

Bu yapılandırma aşağıdaki Java kodu ile eşdeğerdir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .opaqueToken(Customizer.withDefaults())
            );
        return http.build();
    }
 
    @Bean
    public OpaqueTokenIntrospector opaqueTokenIntrospector() {
        return new SpringOpaqueTokenIntrospector(
            "https://my-auth-server.com/oauth2/introspect", "my-client-id", "my-client-secret");
    }
 
}

c. Custom-JWT ile Erişim Koruması

Spring Security’deki OAuth2 Resource Server desteği, özel bir JWT JWT (JSON Web Token – JSON Web Belirteci) olmak üzere her tür Bearer Token (Taşıyıcı Belirteç) için kullanılabilir.

JWT’leri kullanarak bir API’yi korumak için, imzaları doğrulamak ve token’leri decode etmek (çözümlendirmek) için kullanılan bir JwtDecoder’dır. Spring Security, SecurityFilterChain içinde korumayı konfigüre etmek için sağlanan bean’i otomatik olarak kullanacaktır.

1
2
3
4
5
6
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          public-key-location: classpath:my-public-key.pub

Ortak bir anahtar olan my-public-key.pub public key olarak jwt’ye verilmiştir. Spring Boot kullanırken sadece bu işlemi yaml dosyasında yapmamız yeterlidir.

Bu yapılandırma aşağıdaki Java kodu ile eşdeğerdir:

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
@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer((oauth2) -> oauth2
                .jwt(Customizer.withDefaults())
            );
        return http.build();
    }
 
    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(publicKey()).build();
    }
 
    private RSAPublicKey publicKey() {
        // ...
    }
 
}

NimbusJwtEncoder JwtEncoder arayüzünü kullanır.

Custom-JWT ve OAuth JWT yaklaşımlarının ikisinde de JWT kullanılsa da çeşitli noktalarda farklılıkları vardır:

  • En büyük fark OAuth JWT’de authorization server bir gereksinimken, Custom-JWT yaklaşımında zorunlu değildir. Burada resource-server gelen isteği validate edebilir, yani hem authorization server hem resource server olarak davranır.
  • Custom-JWT yaklaşımında token’i oluşturup valide eden resource-server olduğundan token’in içeriği üzerinde daha fazla kontrole sahiptir ancak OAuth2 JWT yaklaşımında server tarafında belirlenen token formatına uymak zorundadır.

d. OAuth 2.0 Resource Server Diagram

Bearer Token Authentication’ın Spring Security içinde nasıl çalıştığını ele alabiliriz. Temel kimlik doğrulamada olduğu gibi “WWW-Authenticate” header’ının(başlık) kimliği doğrulanmamış bir client’a geri gönderildiği Şekil 4’de görülmektedir.

Şekil 4.Security Filter Chain


Şekil 4 basit bir SecurityFilterChain diyagramından oluşmaktadır.

  1. İlk olarak, bir kullanıcı /private ucundaki kaynağa yetkisi olmadığı halde kimliği doğrulanmamış bir istekte bulunur.

  2. Spring Security’nin AuthorizationFilter’ı, AccessDeniedException atarak kimliği doğrulanmamış isteğin reddedildiğini belirtir.

  3. Kullanıcının kimliği doğrulanmadığından ExceptionTranslationFilter isimli yapı kimlik doğrulama işlemlerini başlatır. AuthenticationEntryPoint WWW-Authenticate başlığı gönderen bir BearerTokenAuthenticationEntryPoint örneğidir.[3]

Client “WWW-Authenticate” header’ını aldığında, isteği yeniden bearer token ile denemesi gerektiğini öğrenir.

Şekil 5’deki diyagramda bearer token işlenirken hangi aşamalardan geçtiği gösterilmektedir.


Şekil 5. Bearer Token Akışı


Spring Security Resource Server’ın gelen HTTP isteklerinde yer alan Bearer token’ı nasıl ele alıp doğruladığını, başarısız ve başarılı durumlarda güvenlik zincirinin nasıl devam ettiğini adım adım açıklarsak;

  1. Kullanıcı istekle beraber bearer token’ini yolladığında, BearerTokenAuthenticationFilter bu istekteki token’i alarak bir kimlik doğrulama türü olan BearerTokenAuthenticationToken’i oluşturur.

  2. Devamında HttpServletRequest, ilgili AuthenticationManager’ın seçilmesini sağlayan AuthenticationManagerResolver’a iletilir.

    • BearerTokenAuthenticationToken kimliği doğrulanmak üzere AuthenticationManager’a aktarılır. AuthenticationManager’ın yapısı, JWT veya Opaque Token için yapılandırılmasına göre değişmektedir.
  3. Kimlik doğrulama başarısız olursa,

    • SecurityContextHolder temizlenir,

    • AuthenticationEntryPoint, WWW-Authenticate header’ının tekrar gönderilmesini tetiklemek için çağrılır.[4]

  4. Kimlik doğrulama başarılı olursa,

    • Kimlik doğrulama SecurityContextHolder’da ayarlanır,

    • BearerTokenAuthenticationFilter, uygulama mantığının kaldığı yerden devam etmesi için FilterChain.doFilter(request,response) ögesini çağırır.[4]

e. OAuth2 Client

İlk olarak Spring Boot projemize spring-security-oauth2-client bağımlılığını ekleyelim;

1
2
3
4
5
6
```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
```

f. OAuth2 ile Oturum Açma

Kullanıcıların OAuth2 ile oturum açması oldukça yaygın bir yöntemdir. OpenID Connect 1.0, id_token adı verilen bir token araclığı ile bir OAuth2 istemcisine kimlik doğrulaması yapma, oturum açma olanağı sağlar.

Bazı durumlarda ise OAuth2 kullanıcıların oturum açması için doğrudan kullanılabilir. (GitHub ve Facebook gibi OpenID Connect uygulamayan platformlar).

Aşağıdaki Java kodu, uygulamamızı OAuth2 veya OpenID Connect ile oturum açmamızı sağlayan bir OAuth2 Client’ına çevirir.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // ...
            .oauth2Login(Customizer.withDefaults());
        return http.build();
    }
 
}

Yukarıdaki yapıya ek olarak, uygulamanın bir ClientRegistrationRepository bean kullanılarak yapılandırılması için bir ClientRegistration gerekmektedir. Aşağıda Spring Boot yapılandırma özellikleri kullanılarak bir InMemoryClientRegistrationRepository bean yapılandırılmaktadır:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
  security:
    oauth2:
      client:
        registration:
          my-oidc-client:
            provider: my-oidc-provider
            client-id: my-client-id
            client-secret: my-client-secret
            authorization-grant-type: authorization_code
            scope: openid,profile
        provider:
          my-oidc-provider:
            issuer-uri: https://my-oidc-provider.com

Bu konfigürasyonla beraber iki ek uç desteklenmeye başlanmıştır.

1
2
1. Oturum açmak için kullanılacak bir uç. (Örnek: /oauth2/authorization/my-oidc-client) Oturum açmayı başlatmak ve üçüncü taraf authorization (yetkilendirme) sunucusuna yönlendirme yapmak için kullanılır.
2. İkinci uç ise (Örnek: /login/oauth2/code/my-oidc-client) yetkilendirme (authorization) sunucusu tarafından client’a yönlendirmek için kullanılır ve code parametresi ile id_token ve acces_token’i elde etmek için kullanılır. Kullanılan yapılandırmada **scope** kısmında openid olması OpenID Connect 1.0’ın kullanılması gerektiğini belirtir. Bu sayede Spring Security isteklerin işlenmesi sırasında OIDC’ye özgün bileşenleri (Örnek: OidcUserService) kullanması gerektiğini belirtir.

Bu scope tanımlanmadığı durumda ise Spring Security OAuth2’ya özgü bileşenleri (Örnek: OAuthUserService) kullanacaktır.

1
2
3
4
5
6
7
8
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: # client-id
            client-secret: # client-secret-key

Bu konfigürasyonda ise OpenID Connect kullanmayan uygulamalardan olan GitHub aracılığıyla giriş yapma ve yetkilendirme işlemi sağlanır. Yine bu konfigürasyonla beraber iki ek uç desteklenmeye başlamıştır.

  1. Oturum açmak için kullanılacak bir uç. (Örnek: /oauth2/authorization/my-oauth2-client) Oturum açmayı başlatmak ve üçüncü taraf authorization (yetkilendirme) sunucusuna yönlendirme yapmak için kullanılır.
  2. İkinci uç ise (Örnek: /login/oauth2/code/my-oauth2-client) yetkilendirme (authorization) sunucusu tarafından client’a yönlendirmek için kullanılır ve code parametresi ile id_token ve acces_token’i elde etmek için kullanılır.

g. Korunan Kaynaklara Erişim

OAuth2 tarafından korunan bir üçüncü taraf API’ına istekte bulunmak, OAuth2 Client’ın temel kullanım durumlarından biridir. Bu durum bir Client’ı (Spring Securty’deki OAuth2AuthorizedClient) yetkilendirir ve giden isteğin Authorization header’ına Bearer tokeni ekler bu sayede korunan kaynaklara erişimi sağlar.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // ...
            .oauth2Client(Customizer.withDefaults());
        return http.build();
    }
 
}

Yukarıda bulunan kod parçası uygulamamızı üçüncü taraf bir API’dan korumalı bilgileri talep edebilen bir OAuth2 Client’ı olacak şekilde yapılandırır.

Bu konfigürasyon oturum açmak için bir mekanizma sağlamaz. Oturum açmak için formLogin() gibi yapıların kullanılması gerekir.

Eğer oauth2Client() ile oauth2Login() işlevini birleştiren bir konfigürasyon oluşturmak isteniyorsa ek olarak aşağıdaki şekilde konfigürasyon yapılmalıdır:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
  security:
    oauth2:
      client:
        registration:
          my-oauth2-client:
            provider: my-auth-server
            client-id: my-client-id
            client-secret: my-client-secret
            authorization-grant-type: authorization_code
            scope: message.read,message.write
        provider:
          my-auth-server:
            issuer-uri: https://my-auth-server.com

Spring Security’yi OAuth2 Client özelliklerini destekleyecek şekilde yapılandırdıktan sonra korunan kaynaklara hangi yöntemle erişileceği belirlenerek uygulamayı yapılandırmak gerekecektir. Spring Security, korunan kaynaklara erişme sırasında kullanılabilecek “access token”’ları elde etmek için OAuth2AuthorizedClientManager uygulamasını sağlar.

Mevcutta bir OAuth2AuthorizedClientManager bean’i olmadığında Spring Security varsayılan bir tane oluşturur.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class WebClientConfig {
 
    @Bean
    public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction filter =
                new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        return WebClient.builder()
                .apply(filter.oauth2Configuration())
                .build();
    }
 
}

Her istekteki Authorization header’a Bearer token’leri yerleştirip korumalı kaynaklara erişebilen bir WebClient yapılandırmak için Spring Securty’nin varsayılan OAuth2AuthorizedClientManager’ı kullanılır.

Konfigüre edilen WebClient’ı aşağıdaki şekilde kullanabiliriz:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RestController
public class MessagesController {

    private final WebClient webClient;

    public MessagesController(WebClient webClient) {
        this.webClient = webClient;
    }

    @GetMapping("/messages")
    public ResponseEntity<List<Message>> messages() {
        return this.webClient.get()
                .uri("http://localhost:8090/messages")
                .attributes(clientRegistrationId("my-oauth2-client")) //clientRegistirationId -> parametre olarak verilen client'tan kaynaklara erişim sağlayacak token'ı sağlar  
                .retrieve()
                .toEntityList(Message.class)
                .block();
    }

    public record Message(String message) {
    }

}
    

h. Mevcut Kullanıcıyla Korunan Kaynaklara Erişim

Bir kullanıcı OAuth2 veya OpenID Connect aracılığıyla oturum açtığında, yetkilendirme sunucusu korunan kaynaklara erişmek için kullanılabilecek bir “access token” sağlayabilir. Her iki durum için de tek bir ClientRegistration yapılandırılması gerekir.

Oturum açmak ve korumalı kaynaklara erişim için tek bir ClientRegistration yapılandırılabileceği gibi ayrı ayrı ClientRegistration’lar da yapılandırılabilir.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // ...
            .oauth2Login(Customizer.withDefaults())
            .oauth2Client(Customizer.withDefaults());
        return http.build();
    }
 
}

Uygulamayı, OAuth2 aracılığıyla kullanıcının oturumunu açabildiği ve OAuth2Client aracılığıyla üçüncü taraf bir API’dan korumalı kaynakları talep edebilen bir OAuth2 istemcisi olacak şekilde yapılandırır.

Spring Security’yi OAuth2 Client özelliklerini destekleyecek şekilde yapılandırmanın yanı sıra, korumalı kaynaklara nasıl erişeceğinize karar vermek ve uygulamamızı buna göre yapılandırmanız da gerekecektir. Spring Security, korunan kaynaklara erişmek için kullanılabilecek “access token” elde etmek için OAuth2AuthorizedClientManager yapılandırması sağlar.

Mevcutta bir OAuth2AuthorizedClientManager bean’i olmadığında Spring Security varsayılan bir tane oluşturur.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebClientConfig {
 
    @Bean
    public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction filter =
                new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        return WebClient.builder()
                .apply(filter.oauth2Configuration())
                .build();
    }
 
}

Her istekteki Authorization header’a Bearer token’leri yerleştirip korumalı kaynaklara erişebilen bir WebClient yapılandırmak için Spring Securty’nin varsayılan OAuth2AuthorizedClientManager’ı kullanılır.

Konfigüre edilen WebClient’ı aşağıdaki şekilde kullanabiliriz:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
public class MessagesController {
 
    private final WebClient webClient;
 
    public MessagesController(WebClient webClient) {
        this.webClient = webClient;
    }
 
    @GetMapping("/messages")
    public ResponseEntity<List<Message>> messages() {
        return this.webClient.get()
                .uri("http://localhost:8090/messages")
                .retrieve()
                .toEntityList(Message.class)
                .block();
    }
 
    public record Message(String message) {
    }
 
}

Access Protected Resources başlığında yaptığımız bir önceki örnektekinin aksine Spring Security’ye kullanmak istediğimiz “clientRegistrationId” hakkında bilgi vermemize gerek kalmaz. Bunun nedeni, o anda oturum açmış olan kullanıcıdan bu bilgiyi elde edebilmemizdir.

i. Enable an Extension Grant Type(Uzantı Yetkilendirme Türünü Etkinleştirme)

Spring Security birçok onay türü (grant type) için destek sağlar fakat bu onay türlerinin bazıları varsayılan olarak etkin şekilde bulunmaz. Örneğin jwt-bearer onay türü için destek sağlayan Spring Security, OAuth 2.0 spesifikasyonunun temel bir parçası olmadığı için varsayılan olarak etkin değildir. Standart onay türleri (ör. authorization_code, client_credentials, password, refresh_token) belirli güvenlik kontrolleri ve validasyon adımlarıyla net bir şekilde tanımlandığı için Spring Security bunları varsayılan olarak sağlar. Buna karşın extension grant’ler:

  • Standart dışı akışlar sundukları için ** genelgeçer bir validasyon şeması** tanımlamak zordur.

  • Uygulamaya özel ihtiyaçlara hizmet eder; her projenin gereksinimi farklı olacağından çerçeve, olası tüm senaryoları kapsayan bir konfigürasyonu otomatik olarak etkinleştiremez.

  • Kullanımında oluşacak güvenlik açıkları (eksik token kontrolü, yetkisiz erişim vb.) barındırabileceğinden, kullanıcının tam sorumluluğu aldığı bir aşamaya gelene kadar pasif kalması tercih edilir.

jwt-bearer onay tipini etkinleştirmek için:

1
2
3
4
5
6
7
8
9
    @Configuration
    public class SecurityConfig {
    
        @Bean
        public OAuth2AuthorizedClientProvider jwtBearer() {
            return new JwtBearerOAuth2AuthorizedClientProvider();
        }
    
    }

Access Protected Resources’da olduğu gibi mevcutta bir OAuth2AuthorizedClientManager bean’i olmadığında Spring Security varsayılan bir tane oluşturur.

jwt-bearer kullanılan örnek bir konfigürasyon:

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
@Configuration
public class SecurityConfig {
 
    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {
 
        OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                .authorizationCode()
                .refreshToken()
                .clientCredentials()
                .password()
                .provider(new JwtBearerOAuth2AuthorizedClientProvider())
                .build();
 
        DefaultOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
 
        return authorizedClientManager;
    }
 
}

j. Mevcut Onay Türlerini Özelleştirme

Sprig Security, varsayılan onay türlerini (grant type) yeniden tanımlamaya gerek kalmadan mevcut bir onay türünü özelleştirme fırsatı sunar.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class SecurityConfig {
 
    @Bean
    public OAuth2AuthorizedClientProvider clientCredentials() {
        ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider =
                new ClientCredentialsOAuth2AuthorizedClientProvider();
        authorizedClientProvider.setClockSkew(Duration.ofMinutes(5));
 
        return authorizedClientProvider;
    }
 
}

client_credentials onay türü için OAuth2AuthorizedClientProvider’ın saat uyuşmazlığını (clock skew) özelleştirmek istiyorsak yukarıdaki gibi bir konfigürasyon yapabiliriz.

Clock Skew: Authorization Server ile Resource Server veya Client saatleri arasındaki potansiyel oluşabilecek zaman farkını ifade eder.

Token’lerin sona erme sürelerini doğrularken sistemler arasındaki küçük zaman farklarını hesaba katmak ve küçük saat farklılıklarından kaynaklanan hataları önlemek için dikkate alınması gerekir.

Clock skew sebebi ile oluşabilecek hatalar:

  • Erken red (premature rejection): Client, Authorization Server’dan aldığı token’ın “not before” (nbf) zamanından birkaç saniye önce kullanmaya kalktığında token henüz geçerli değil sayılarak reddedilebilir.

  • Gecikmiş geçerlilik (delayed validity): Token’ın süresinin dolduğu (exp) anlaşıldığı zaman ile Resource Server’ın kontrolü arasında zaman farkı olduğunda süresi çoktan dolmuş bir token hâlâ geçerli sayılabilir.

  • Oturum/yenileme sorunları: Refresh token’lar, client tarafında “hala geçerli” görünürken Authorization Server’da süresi dolmuş olabilir; bu da 401 hatalarına neden olur.

Çözüm olarak ;

  • NTP Senkronizasyonu: Tüm sunucularınızın (Auth Server, Resource Server, Client) Network Time Protocol (NTP) üzerinden aynı zaman kaynağına bakacak şekilde konfigüre edilmesi, gerçek dakika–saniye tutarsızlıklarını minimize eder.

  • Toleranslı doğrulama (grace period): Token zaman damgalarında (nbf/exp) birkaç saniyelik (örn. 30–60 sn) bir tolerans penceresi tanımlayarak, saat farklarının neden olduğu küçük sapmaların reddedilmesini önleyebilirsiniz.

3.Token-Based Authentication (Belirteç Tabanlı Kimlik Doğrulama)

Token-Based authentication, kullanıcının kim olduğunu doğrulaması ve ardından eşsiz bir access token almasına imkan sağlayan protokoldür. Token aktif olduğu sürece kullanıcı uygulamada token’ın erişim sağladığı içeriklere bilgilerini tekrar tekrar girmeden erişebilir. Yani token aktif kaldıkça erişim sağlar, logout olunduğunda inaktif olur. Token-Based authentication yaklaşımı uygulamadaki içerikler, uygulamada yapılan işlemler ve diğer özellikler üzerinde standart password-based ya da server-based yaklaşımlara göre daha detaylı bir kontrol sağlar. İkinci bir güvenlik katmanı oluşturur. Belirli özellikler için farklı token’lar oluşturarak, kullanıcıya verilen token ile kullanıcıdan beklenmeyen işlemlerin yapılmasının da önüne geçilir. Bu noktada authorization için de etkilidir.

Authentication token’lardan önce kullanılan geleneksel yaklaşımlarda kullanıcının doğru işlemi yapacağı varsayılmaktaydı. Doğru kullanıcının doğru işi yapıyor olması varsayımdan ibaret olduğundan bu yaklaşım fazla efektif değildi. Şifreler değerlendirildiğinde, kullanıcı tarafından belirtilen kısıtlara uyularak oluşturulan şifreler sürekli olarak hatırlanmalı ve erişime ihtiyaç duyulduğunda tekrar tekrar girilmelidir. Şifrenin çalınma ya da unutulma ihtimaline karşın şifrelerin kaydedilmesi ve diğer hesaplar için de aynı şifrelerin kullanılması yoluna gidildiğinde ise efektif olmayan sonuçlar ortaya çıkar. Şifrenin kağıtlara not edildiği düşünüldüğünde bu oldukça karmaşık olacaktır. Diğerinde ise şifrenin ifşa olması durumunda bir çok hesap savunmasız kalacaktır. Şifrelerde sürekli değişim yapılması gerekince de bir noktada kullanıcı şifrede çok ufak değişiklikler yapmaya yönelir ve bu da çok güvenli ve efektif değildir. Geleneksel yaklaşımların oldukça zorlayıcı tarafları bulunmaktadır.

Farklı authentication token tipleri bulunmaktadır.

  • Connected (Bağlı): Sisteme takılan external cihazlar ile authenticate olunuyorsa bu connected authentication’dır. E-imza örnek verilebilir.
  • Contactless (Temassız) : Bir cihaz server’a yakın ve takılmadan authentication için kullanılıyorsa bu contactless authentication’dır. Microsoft magic ring örnek verilebilir.
  • Disconnected (Bağlantısız): Uzak mesafelerden server sisteme takılmadan ya da yakınında temassız iletişime geçilmeden authentication olunan durumlar disconnected authentication’dır. 2D Secure örnek gösterilebilir.

Token authentication adımları:

  • Request: Kullanıcı erişim için authenticated olur. Bu aşama password ile ya da başka bir yöntemle olabilir.
  • Verification: Server kullanıcıyı doğrular.
  • Tokens: Server token oluşturur, gerekli izinlerle ilişkilendirir ve kullanıcıya döner.
  • Storage: Token, işlem devam ettiği sürece saklanır. Şekil 6’da, kullanıcı kimlik bilgilerini istemciye (Client) iletir. İstemci bu credential’ları(kimlik bilgisi) Authorization Server’a gönderir ve Access Token ile Refresh Token alır. Ardından elde ettiği Access Token (ve gerekirse Refresh Token) ile Resource Server’a istek yapar. Resource Server token’ı doğrulayıp korunan kaynağı döner; token süresi dolmuşsa yeni bir Access Token sağladıktan sonra cevabı iletebilir.


Şekil 6.Token ile Yetkilendirme

3.1 Json Web Token (JWT)

Şekil 7’de bir Json Web Token yapısı gösterilmektedir. JWT, bir token-based authorization standartıdır. Header payload ve signature partlarından oluşur. Bu partlar token içinde dot-seperated şekilde bulunur. Header’da bulunan alg kısmı hash algoritmasını belirtir. Typ ise type demektir. Örnekte type JWT verilmiştir. Bu kısım Base64 ile şifrelenip token’ın ilk kısmını oluşturur. Payload kısmında gerçek veri bulunur. Gerçek verinin yanında bu kısım claimlerden oluşur. Bu claimlerden bazıları iss (issuer), exp (expiration time), sub (subject), aud (audience). Bu claimler bağlıdır ama kullanılması önerilmektedir. payload alanı da Base64 ile encode edilir ve token’ın 2. Partını oluşturur. Son part ise signature alanıdır. Bu kısım için header, payload ve private key gerekmektedir. Header, payload ve private key alanları header’da bulunan algoritma kullanılarak encode edilir. JWT’nin doğrulama işleminde ise, gelen token noktalara göre ayrılır. Header ve payload alanları private key ile imzalanıp signature alanıyla karşılaştırılır ve doğrulama yapılır.


Şekil 7. JWT Yapısı

JWT, bir token-based authorization standartıdır. Header payload ve signature partlarından oluşur. Bu partlar token içinde dot-seperated şekilde bulunur. Header’da bulunan alg kısmı hash algoritmasını belirtir. Typ ise type demektir. Örnekte type JWT verilmiştir. Bu kısım Base64 ile şifrelenip token’ın ilk kısmını oluşturur. Payload kısmında gerçek veri bulunur. Gerçek verinin yanında bu kısım claimlerden oluşur. Bu claimlerden bazıları iss (issuer), exp (expiration time), sub (subject), aud (audience). Bu claimler bağlıdır ama kullanılması önerilmektedir. payload alanı da Base64 ile encode edilir ve token’ın 2. Partını oluşturur. Son part ise signature alanıdır. Bu kısım için header, payload ve private key gerekmektedir. Header, payload ve private key alanları header’da bulunan algoritma kullanılarak encode edilir. JWT’nin doğrulama işleminde ise, gelen token noktalara göre ayrılır. Header ve payload alanları private key ile imzalanıp signature alanıyla karşılaştırılır ve doğrulama yapılır.

a. Avantajlar

  • Boyut: Token’lar boyut olarak küçük olduğundan 2 entity arasında çok hızlı şekilde transfer edilebilir.
  • Kolaylık: Token’lar neredeyse her yerden üretilebilir ve tek bir server tarafından doğrulanmak zorunda değillerdir.
  • Kontrol: Token’lar ile kimin uygulamaya erişebileceği, izinlerin ne kadar süre kalacağı ve oturumdayken ne yapmaya yetkili olduğu kontrol edilir.

    b. Dezavantajlar

  • Tek Anahtar: JWT tek bir anahtara dayanır ve bu anahtar ele geçirilirse tüm sistem risk altına girer.
  • Karmaşıklık: Token’lar kolay anlaşılabilir bir yapıda değildir. Eğer geliştiricimin kriptografik imza algoritmaları hakkında sağlam bir bilgi birikimine sahip değilse istemeden de olsa sistemi risk altına sokabilir.
  • Sınırlamalar: Mesajlar tüm istemcilere gönderilemez ve istemciler sunucu tarafından yönetilemez.

c. Neden Yetkilendirme Token’ları Kullanılmalı?

Aşağıdaki gibi sistemler için yetkilendirme token’ları için faydalıdır:

  • Geçici erişim izni veren sistemler: Kullanıcı tabanı tarihe, saate veya özel bir etkinliğe göre değişiklik gösteren uygulamalar. Erişimi tekrar tekrar vermek ve iptal etmek çok yorucudur olduğundan token’lar faydalı olabilir.
  • Ayrıntılı erişim gerektiren uygulamalar: Kullanıcıya göre değil uygulamadaki alanlara göre izin veren uygulamalar. Parolalar bu kadar ince ayarlı ayrıntılara izin veremez.
  • Örneğin, çevrimiçi bir günlük işletiyorsunuz. Herkesin yalnızca bir belgeyi okuyup yorum yapmasını istiyorsunuz, diğerlerini değil. Tokenlar buna izin verebilir.
  • Güvenlik açısından tehdit altında olan uygulamalar: Hassas veriler içeren uygulamalar için sadece parola koruması yeterli değildir. Parçalı erişim izinleri gerekir.

d. Authentication Token İyi Pratikler

Authentication tokenların kullanımında iyi pratikleri takip ederek daha efektif çalışması sağlanabilir.

  • Gizli: Kullanıcılar authentication cihazlarını paylaşamaz ya da aktaramaz. Şifre gibi düşünülebilir. Şifrenin paylaşılamayacağı gibi bu cihazlar da paylaşılamaz.
  • Güvenli: Token ve server arası iletişim HTTPS aracılığıyla güvende olmalıdır. Şifreli haberleşme kullanılmalıdır.
  • Test edilmiş: Periyodik olarak token testleri sağlanmalı ve sistem için işlevi test edilmeli. Eğer bir sorun tespit edilirse hızlıca çözülmeli.
  • Uygun: Özel kullanım durumları için doğru token tipleri seçilmeli. Örneğin JWT token’ları session token olarak kullanıma uygun değildir.
  • Authentication token seçimi ya da hakkında alınacak kararlar aceleye getirilmemeli. Üstüne çalışmalar yapılıp doğru seçim olduğuna emin olunduktan sonra gerekli işlem yapılmalıdır.

e. JWT Konfigürasyonu

Öncelikle, JWT oluşturma aşamasında kullanılacak olan public ve private key çiftleri oluşturulmalıdır. Key çifti oluşturulduktan sonra, uygulamada kullanılmak üzere bu key çiftinin dosya yolları application.properties dosyasına tanımlanıp uygulama içinde kullanılabilmesi için bu yolu application.properties dosyasından kullanacak olan record oluşturulur.

1
2
rsa.private-key=classpath:certs/private.pem
rsa.public-key=classpath:certs/public.pem

Oluşturduğumuz bu keyler güvenli bir şekilde saklanmalıdır. Bunun için; Donanım Güvenlik Modülleri (HSM) veya Bulut KMS (Key Management Service) çözümleri kullanılabilir. Bu sayede anahtarlar merkezi olarak yönetilir, yetkisiz erişime karşı korunur ve güvenlik politikaları ile entegre edilir.

  • HSM’de (Hardware Security Module) veya AWS KMS, Azure Key Vault, Google Cloud KMS gibi bulut tabanlı Anahtar Yönetim Hizmetleri’nde tutmak mantıklı olabilir. Bu servisler, anahtarların bellek dışı ortamda (off-host) korunmasını sağlar ve uygulamanın doğrudan anahtara erişimine gerek kalmadan imzalama işlemleri gerçekleştirilmesine izin verir.

  • JKS (Java KeyStore) veya PKCS#12 formatında bir dosyada tutup, güçlü bir parola ile şifrelenebilir. Dosyanın yetkisiz erişime karşı korunması için işletim sistemi düzeyinde (chown/chmod) sıkı dosya izinleri uygulanır.

Ayrıca parolaları, ilgili pathleri veya access token’lar gibi hassas bilgileri doğrudan uygulama kodunda bulundurmamak önemlidir. Bunun yerine Docker Secrets, Kubernetes Secrets veya Vault gibi gizli yönetim araçlarıyla runtime içerisinde kullanılabilir. Belirli periyodlarla keylerin güncellenmesi de yapılması gereken önemli işlemlerdendir. Tolerans için eski keylerin geçici bir süre daha geçerli olduğu bir key havuz kullanımı da olası tekniklerdendir.

1
2
3
@ConfigurationProperties(prefix = "rsa")
public record RsaKeyProperties(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
}

Bu şekilde public ve private key ikilisi oluşturulup projede kullanılabilmesi için gerekli altyapı hazırlandı. Şimdi JWT için konfigürasyon kısmına geçilir. Burada öncelikle sayfa ilk açıldığında giriş yapılabilmesi için bir default user tanımlaması yapılıyor.

1
2
3
4
5
6
7
8
9
10
@Bean
    public InMemoryUserDetailsManager defaultUser() {
        return new InMemoryUserDetailsManager(
                User.withUsername("admin")
                        //by default, encoded password passed, use {noop} to give it directly
                        .password("{noop}pass")
                        .authorities("read")
                        .build()
        );
    }

Bu tanımlamanın ardından, JWT authentication’ı konfigüre edecek olan security filter chain tanımlanır.

1
2
3
4
5
6
7
8
9
10
11
12
13
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
        // never disable csrf, this is for demo app
        .csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        // jwt decoder needs to be initialized in config
        // we need to create public/private key pair
        .oauth2ResourceServer(configurer -> configurer.jwt(Customizer.withDefaults()))
        .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .httpBasic(Customizer.withDefaults())
        .build();
        }

Security filter chain tanımlandıktan sonra, JWT encode ve decode işlemi yapacak olan encoder ve decoder’lar tanımlanmalıdır. Burada encoder ve decoder olarak NimbusJWTEncoder ve NimbusJWTDecoder kullanılmıştır. Encode ve decode aşamaları yukarıda JWT anlatımında belirtilmiştir.

Encoder

1
2
3
4
5
6
@Bean
public JwtEncoder jwtEncoder() {
    JWK jwk = new RSAKey.Builder(rsaKeyProperties.publicKey()).privateKey(rsaKeyProperties.privateKey()).build();
    JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
    return new NimbusJwtEncoder(jwkSource);
}

Decoder

1
2
3
4
@Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(rsaKeyProperties.publicKey()).build();
    }

JWT konfigürasyonları bu şekilde tamamlandıktan sonra, JWT üretecek bir servise ihtiyaç vardır. Bu servis oluşturulur. Kullanıcı login olduktan sonra bu servis tetiklenir ve token oluşturulup kullanıcıya döndürülür.

Token Service

1
2
3
4
5
6
7
8
9
10
11
12
public String generateToken(Authentication authentication) {
        Instant now = Instant.now();
        String scope = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(" "));
        JwtClaimsSet jwtClaimsSet = JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now)
                .expiresAt(now.plus(1, ChronoUnit.HOURS))
                .subject(authentication.getName())
                .claim("scope", scope)
                .build();
        return this.jwtEncoder.encode(JwtEncoderParameters.from(jwtClaimsSet)).getTokenValue();
    }

4. Sonuç

Bu yazıda temel olarak üç farklı kimlik doğrulama ve yetkilendirme yaklaşımını ele aldık: Basic Authentication, OAuth 2.0 akışları ve Token-Based Authentication (özellikle JWT). Basic Authentication, en hızlı ve kolay entegrasyon imkânını sunarken, kullanıcı adı ve parola bilgilerinin her istekte iletilmesi ve parolaların kolayca açığa çıkabilir olması gibi başlıca güvenlik dezavantajları barındırır. Bu nedenle genellikle yalnızca prototip geliştirme veya iç ağlarda sınırlı servislerde tercih edilir. Öte yandan OAuth 2.0, authorization_code, client_credentials ve refresh_token gibi yerleşik akışları sayesinde hem kullanıcı odaklı hem de servisler arası senaryolarda esneklik sağlar; Spring Security, bu akışlar için otomatik validasyon ve güvenlik kontrolleri sunar, ancak özel ihtiyaçlar için tanımlanan extension grant’ler mutlaka dikkatle yapılandırılmalı ve ek validasyon adımları eklenmelidir.

Token-Based Authentication yaklaşımında ise, JWT kullanarak stateless bir mimari elde etmek mümkün olur ve böylece mikroservisler arası çapraz entegrasyon ve ölçeklenebilirlik artar. Ancak JWT’lerin güvenli saklanması, anahtar yönetimi, token süresi dolduğunda iptal edilmesi ve olası replay saldırılarına karşı revocation mekanizmalarının iyi planlanması gerekir.

Serinin bir sonraki yazısına API Key ve Session-Based Authentication Yaklaşımları adresinden ulaşabilirsiniz.


*Bu yazının hazırlanmasında katkı sağlayan teknik gözden geçirme için Rabia Nur ÖNAL’a ve Ahmet Burak KAPLAN’a, içerik düzenlemelerinde destek sunan Kübra ERTÜRK’e teşekkür ederiz. Sağladıkları geri bildirimler, yazının hem teknik doğruluğunu hem de okunabilirliğini artırmada önemli rol oynamıştır.

Kaynakça

[1] Spring Security Documentation, URL: https://docs.spring.io/spring-security/reference/index.html (Erişim zamanı; Ocak 12, 2024).

[2] Spring Documentation, URL: https://docs.spring.io/spring-framework/reference/ (Erişim zamanı; Şubat 2, 2024).

[3] Spring Security Documentation, URL: https://spring.io/guides/topicals/spring-security-architecture (Erişim zamanı; Şubat 15, 2024).

[4] Spring Framework Documentation, URL: https://docs.spring.io/spring-framework/docs/current/reference/html/web.html (Erişim zamanı; Mart 5, 2024).

[5] Baeldung, “Spring Security Basic Authentication”, URL: https://www.baeldung.com/spring-security-basic-authentication (Erişim zamanı; Ocak 24, 2024).