← Kursa Dön
📄 Text · 18 min

OAuth2 Resource Server Temelleri

Giriş — Neden Bu Konu Önemli?

Önceki derslerde kendi JWT auth sistemimizi sıfırdan kurduğumuz — JwtService ile token oluşturduk, JwtAuthenticationFilter ile doğruladık, refresh token mekanizması ekledik. Bu yaklaşım tek bir uygulama için yeterlidir. Peki ya birden fazla uygulamanız varsa? Mobil app, web app, admin paneli, microservice'ler... Her birinde ayrı JWT sistemi mi kuracaksınız?

İşte OAuth2 tam bu noktada devreye girer. OAuth2, yetkilendirme (authorization) için endüstri standardı protokoldür (RFC 6749). Bir uygulamanın, kullanıcının şifresini bilmeden, başka bir servisteki kaynaklarına erişmesini sağlar. "Google ile Giriş Yap", "GitHub ile Bağlan" gibi özellikler OAuth2 üzerine kuruludur.

Gerçek Hayat Analojisi: Bir otele giriş düşünün. Kendi JWT sisteminiz, otelin kapısında kendi kilit sisteminizi kurmaktır — sadece sizin anahtarınız çalışır. OAuth2 ise uluslararası bir anahtar kart sistemidir — aynı kartla dünyanın her yerindeki otellere girebilirsiniz. Keycloak/Auth0 "anahtar kart üreten merkez", sizin Spring Boot API'niz ise "otel kapısındaki kart okuyucu" (Resource Server) rolündedir.


OAuth2 Rolleri

OAuth2 ekosisteminde dört temel rol vardır:

┌──────────────────┐      ┌───────────────────────┐
│  Resource Owner  │      │ Authorization Server  │
│  (Kullanıcı)     │      │ (Keycloak, Auth0,     │
│                  │      │  Google, Spring Auth)  │
└────────┬─────────┘      └───────────┬───────────┘
         │ 1. Login                   │ 2. Token ver
         ▼                            ▼
┌──────────────────┐      ┌───────────────────────┐
│     Client       │─────▶│   Resource Server     │
│ (React, Mobile,  │  3.  │ (Sizin Spring Boot    │
│  başka servis)   │Token │  API'niz)             │
│                  │ile   │                       │
│                  │istek │                       │
└──────────────────┘      └───────────────────────┘
  • Resource Owner: Kullanıcı — verinin sahibi

  • Client: Frontend uygulaması, mobil app veya başka bir servis

  • Authorization Server: Token üreten merkezi kimlik servisi (Keycloak, Auth0, Okta, Spring Authorization Server, Google)

  • Resource Server: Token'ı doğrulayarak korunan kaynakları sunan API — bizim Spring Boot uygulamamız

Bu derste odak noktamız Resource Server tarafıdır — Authorization Server'dan gelen token'ı doğrulayıp API'mizi korumak.


Kendi JWT Auth vs OAuth2

Neden kendi JWT auth sistemi yerine OAuth2 kullanmalısınız?

ÖzellikKendi JWT AuthOAuth2 Resource Server
Token üretimiSizin uygulamanızAuthorization Server (Keycloak, Auth0)
Key yönetimiManuel (secret key)Otomatik (JWKS endpoint)
Token doğrulamaJwtService sınıfınızSpring Security otomatik
SSO desteği❌ Yok✅ Tek login, çoklu uygulama
Social loginManuel implementasyon✅ Hazır (Google, GitHub vb.)
Token revocationBlacklist gerekirAuthorization Server yönetir
Key rotationManuel güncelleme + downtimeJWKS ile otomatik
KarmaşıklıkDüşük (tek uygulama)Orta-yüksek (ek altyapı)
Uygun olduğu yerTek uygulama, basit authMicroservices, enterprise

⚠️ Ne zaman hangisi? Tek bir Spring Boot uygulaması ve basit auth gerekiyorsa kendi JWT sisteminiz yeterlidir. Birden fazla uygulama, SSO, social login veya enterprise gereksinimler varsa OAuth2 + Keycloak/Auth0 tercih edin.


Spring Boot Resource Server Kurulumu

Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

Bu tek dependency ile Spring Security'nin OAuth2 resource server desteği aktif olur.

application.yml Konfigürasyonu

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # Authorization Server'ın issuer URI'si
          # Spring Boot buradan otomatik olarak JWKS endpoint'ini keşfeder
          issuer-uri: https://auth.example.com/realms/myapp

          # Alternatif: JWKS URI'sini doğrudan belirt
          # jwk-set-uri: https://auth.example.com/realms/myapp/protocol/openid-connect/certs

Nasıl Çalışır? (Otomatik Keşif)

  1. Spring Boot başlangıçta issuer-uri adresindeki .well-known/openid-configuration endpoint'ini çağırır

  2. Bu endpoint'ten JWKS (JSON Web Key Set) URI'sini öğrenir

  3. JWKS'den Authorization Server'ın public key'lerini indirir

  4. Gelen JWT token'ları bu public key'ler ile doğrular (signature verification)

Spring Boot Başlangıcında:
  1. GET https://auth.example.com/realms/myapp/.well-known/openid-configuration
     → {"jwks_uri": "https://auth.example.com/.../certs", ...}

  2. GET https://auth.example.com/.../certs
     → {"keys": [{"kty":"RSA","kid":"abc123","n":"...","e":"AQAB"}]}

Her İstek Geldiğinde:
  3. Token'ın header'ındaki "kid" ile doğru public key'i bul
  4. Token'ın signature'ını public key ile doğrula
  5. Token'ın exp, iss, aud claim'lerini kontrol et
  6. Doğruysa → Authentication başarılı, yanlışsa → 401

💡 Kendi JWT vs OAuth2 farkı: Kendi sistemimizde secret key (symmetric) kullanıyorduk — aynı key ile hem imzalama hem doğrulama. OAuth2'de asymmetric key kullanılır — Authorization Server private key ile imzalar, Resource Server public key ile doğrular. Bu sayede Resource Server hiçbir zaman token üretemez, sadece doğrulayabilir.


SecurityFilterChain Konfigürasyonu

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())  // Stateless API — CSRF gerekmez
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**", "/actuator/health").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            )
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.setContentType("application/json");
                    response.getWriter().write("""
                        {"error": "unauthorized", "message": "Token geçersiz veya eksik"}
                    """);
                })
            )
            .build();
    }
}

Dikkat edilmesi gerekenler:

  • .oauth2ResourceServer(...) kendi JwtAuthenticationFilter'ımız yerine geçer — Spring Security otomatik olarak token'ı parse eder, doğrular ve Authentication nesnesini oluşturur

  • SessionCreationPolicy.STATELESS — her istek bağımsız, session yok

  • Exception handling ile 401 yanıtı özelleştirilebilir


JWT Claim'lerinden Roller Çıkarma

Authorization Server'lar rolleri farklı claim'lerde gönderir. Keycloak realm_access.roles, Auth0 permissions, custom server roles kullanır. Spring Boot'a bu mapping'i öğretmemiz gerekir:

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
        new JwtGrantedAuthoritiesConverter();
    // Default: "scope" veya "scp" claim'ini okur → SCOPE_xxx authority'leri oluşturur

    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(jwt -> {
        Collection<GrantedAuthority> authorities = new ArrayList<>();

        // 1. Scope'ları ekle (varsa)
        authorities.addAll(grantedAuthoritiesConverter.convert(jwt));

        // 2. Keycloak realm roller
        Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
        if (realmAccess != null) {
            @SuppressWarnings("unchecked")
            List<String> roles = (List<String>) realmAccess.get("roles");
            if (roles != null) {
                roles.stream()
                    .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
                    .forEach(authorities::add);
            }
        }

        // 3. Custom roller (kendi auth server'ınız için)
        List<String> customRoles = jwt.getClaimAsStringList("roles");
        if (customRoles != null) {
            customRoles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
                .forEach(authorities::add);
        }

        return authorities;
    });

    return converter;
}

Farklı Auth Server'lar İçin Rol Claim Formatları

Auth ServerRol Claim YoluÖrnek
Keycloakrealm_access.roles["admin", "user"]
Auth0permissions veya custom claim["read:users", "write:users"]
Oktagroups["Admin", "User"]
Customroles["ROLE_ADMIN", "ROLE_USER"]

JWT Decoder Özelleştirme

Token doğrulama kurallarını özelleştirmek için custom JwtDecoder:

@Bean
public JwtDecoder jwtDecoder() {
    NimbusJwtDecoder decoder = NimbusJwtDecoder
        .withJwkSetUri("https://auth.example.com/realms/myapp/protocol/openid-connect/certs")
        .build();

    // Issuer validator
    OAuth2TokenValidator<Jwt> issuerValidator =
        JwtValidators.createDefaultWithIssuer("https://auth.example.com/realms/myapp");

    // Custom audience validator — token bu API için mi?
    OAuth2TokenValidator<Jwt> audienceValidator = token -> {
        List<String> audiences = token.getAudience();
        if (audiences != null && audiences.contains("myapp-api")) {
            return OAuth2TokenValidatorResult.success();
        }
        return OAuth2TokenValidatorResult.failure(
            new OAuth2Error("invalid_audience", "Token bu API için değil", null));
    };

    // Birden fazla validator birleştir
    OAuth2TokenValidator<Jwt> combinedValidator =
        new DelegatingOAuth2TokenValidator<>(issuerValidator, audienceValidator);

    decoder.setJwtValidator(combinedValidator);
    return decoder;
}

Scope-Based ve Role-Based Authorization

OAuth2'de scope'lar client'ın hangi kaynaklara erişebileceğini, roller ise kullanıcının ne yapabileceğini belirler:

@RestController
@RequestMapping("/api")
public class ResourceController {

    // "read" scope'u gerektirir
    @GetMapping("/documents")
    @PreAuthorize("hasAuthority('SCOPE_read')")
    public List<Document> getDocuments() { ... }

    // "write" scope'u gerektirir
    @PostMapping("/documents")
    @PreAuthorize("hasAuthority('SCOPE_write')")
    public Document createDocument(@RequestBody DocumentDto dto) { ... }

    // Hem role hem scope kontrolü
    @DeleteMapping("/documents/{id}")
    @PreAuthorize("hasRole('ADMIN') and hasAuthority('SCOPE_admin')")
    public void deleteDocument(@PathVariable Long id) { ... }

    // JWT'den kullanıcı bilgilerini al
    @GetMapping("/me")
    public Map<String, Object> currentUser(@AuthenticationPrincipal Jwt jwt) {
        return Map.of(
            "subject", jwt.getSubject(),
            "email", Objects.toString(jwt.getClaimAsString("email"), "N/A"),
            "issuedAt", jwt.getIssuedAt(),
            "expiresAt", jwt.getExpiresAt(),
            "claims", jwt.getClaims()
        );
    }
}

@AuthenticationPrincipal ile JWT Erişimi

// JWT claim'lerine doğrudan erişim
@GetMapping("/profile")
public UserProfile getProfile(@AuthenticationPrincipal Jwt jwt) {
    String userId = jwt.getSubject();
    String email = jwt.getClaimAsString("email");
    String name = jwt.getClaimAsString("preferred_username");

    return userService.getProfile(userId);
}

// Custom principal dönüşümü
@GetMapping("/dashboard")
public Dashboard getDashboard(
    @AuthenticationPrincipal(expression = "subject") String userId) {
    return dashboardService.getForUser(userId);
}

Birden Fazla Issuer Desteği

Bazı uygulamalar birden fazla Authorization Server'dan token kabul etmek isteyebilir (örneğin hem Keycloak hem Google):

@Bean
public JwtDecoder jwtDecoder() {
    // İki farklı issuer'dan token kabul et
    JwtDecoder keycloakDecoder = NimbusJwtDecoder
        .withJwkSetUri("https://keycloak.example.com/.../certs")
        .build();

    JwtDecoder googleDecoder = NimbusJwtDecoder
        .withJwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
        .build();

    // Issuer'a göre doğru decoder'ı seç
    return token -> {
        try {
            // İlk olarak Keycloak ile dene
            return keycloakDecoder.decode(token);
        } catch (JwtException e) {
            // Keycloak başarısız → Google ile dene
            return googleDecoder.decode(token);
        }
    };
}

💡 Spring Security'nin JwtIssuerAuthenticationManagerResolver sınıfı bu senaryoyu daha temiz şekilde çözer:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    JwtIssuerAuthenticationManagerResolver resolver =
        JwtIssuerAuthenticationManagerResolver.fromTrustedIssuers(
            "https://keycloak.example.com/realms/myapp",
            "https://accounts.google.com"
        );

    return http
        .oauth2ResourceServer(oauth2 -> oauth2
            .authenticationManagerResolver(resolver)
        )
        .build();
}

Test Yazma

@WebMvcTest(ResourceController.class)
class ResourceControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockUser(roles = "USER")
    void shouldAllowAuthenticatedUser() throws Exception {
        mockMvc.perform(get("/api/documents"))
            .andExpect(status().isOk());
    }

    @Test
    void shouldRejectUnauthenticatedRequest() throws Exception {
        mockMvc.perform(get("/api/documents"))
            .andExpect(status().isUnauthorized());
    }

    // JWT mock ile test
    @Test
    void shouldAcceptValidJwt() throws Exception {
        mockMvc.perform(get("/api/me")
                .with(jwt()
                    .jwt(builder -> builder
                        .subject("user123")
                        .claim("email", "user@example.com")
                        .claim("realm_access", Map.of("roles", List.of("user")))
                    )
                    .authorities(new SimpleGrantedAuthority("ROLE_USER"))
                ))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.subject").value("user123"));
    }
}

Keycloak ile Pratik Entegrasyon

Keycloak, en popüler open-source Authorization Server'dır. Docker ile hızlıca kurulabilir:

# Keycloak'u Docker ile başlat
docker run -d \
  --name keycloak \
  -p 8180:8080 \
  -e KEYCLOAK_ADMIN=admin \
  -e KEYCLOAK_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:24.0 start-dev

Keycloak Konfigürasyonu

  1. http://localhost:8180 adresine gidin (admin/admin)

  2. Yeni bir Realm oluşturun: myapp

  3. Client oluşturun: myapp-api (Client type: OpenID Connect)

  4. User oluşturun ve şifre belirleyin

  5. Realm roles oluşturun: admin, user

  6. Kullanıcıya rolleri atayın

Spring Boot Konfigürasyonu

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8180/realms/myapp

Token Alma ve Test

# Keycloak'tan token al (Resource Owner Password Grant — sadece test için)
curl -X POST http://localhost:8180/realms/myapp/protocol/openid-connect/token \
  -d "client_id=myapp-api" \
  -d "grant_type=password" \
  -d "username=testuser" \
  -d "password=test123" \
  -d "scope=openid"

# Yanıt:
# {
#   "access_token": "eyJhbGciOiJSUzI1NiIs...",
#   "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
#   "token_type": "Bearer",
#   "expires_in": 300
# }

# Token ile API'ye istek at
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
  http://localhost:8080/api/me

Keycloak Token'ının İçeriği

{
  "sub": "f5e8b4a2-1234-5678-abcd-1234567890ab",
  "realm_access": {
    "roles": ["admin", "user", "default-roles-myapp"]
  },
  "scope": "openid email profile",
  "email": "testuser@example.com",
  "preferred_username": "testuser",
  "given_name": "Test",
  "family_name": "User",
  "iss": "http://localhost:8180/realms/myapp",
  "exp": 1709321123,
  "iat": 1709320823
}

Bu token'ı doğru şekilde parse etmek için JwtAuthenticationConverter'da realm_access.roles claim'ini okuyan kodu kullanın (yukarıda gösterildi).


Hata Yönetimi

OAuth2 Resource Server'da özel hata yanıtları:

@Configuration
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
                .authenticationEntryPoint((request, response, exception) -> {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.setContentType("application/json");
                    response.getWriter().write("""
                    {
                        "error": "unauthorized",
                        "message": "Geçersiz veya eksik Bearer token",
                        "details": "%s"
                    }
                    """.formatted(exception.getMessage()));
                })
                .accessDeniedHandler((request, response, exception) -> {
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    response.setContentType("application/json");
                    response.getWriter().write("""
                    {
                        "error": "forbidden",
                        "message": "Bu işlem için yetkiniz yok"
                    }
                    """);
                })
            )
            .build();
    }
}

Özet

  • OAuth2 Resource Server dışarıdan alınan JWT token'ları doğrulayarak API'nizi korur

  • Authorization Server (Keycloak, Auth0) token üretir, Resource Server (Spring Boot) public key ile doğrular

  • issuer-uri ayarı ile Spring Boot otomatik olarak JWKS endpoint'ini keşfeder ve public key'leri indirir

  • JwtAuthenticationConverter ile claim-to-authority mapping yapılır — her Auth Server farklı format kullanır

  • Scope-based (SCOPE_read) ve role-based (ROLE_ADMIN) authorization birlikte kullanılabilir

  • @AuthenticationPrincipal Jwt ile controller'da token claim'lerine doğrudan erişilebilir

  • Tek uygulama + basit auth → kendi JWT sisteminiz yeterli; microservices + SSO → OAuth2 tercih edin