Bölüm 2 — Keycloak Kurulum ve Mikroservis Güvenliği

Gokhan Konuk
Akbank Teknoloji
Published in
5 min readOct 6, 2023

--

Bu yazı serisinin ilk bölümünde Akbank Teknoloji olarak alt yapılarımızda kullandığımız Keycloak’un temel kavramlarını, kimlik ve erişim yönetimi alanındaki önemini hem kurumsal hem yazılım mimarisi açısından ele aldık.

İkinci bölümde, Keycloak’u kurmaya ve temel yapılandırmayı yapmaya başlayacağız. Spring Boot ile geliştirdiğimiz mikroservislerimizin nasıl secure (güvenli) hale getirildiğini ve bunların auth/authz (doğrulama/yetkilendirme) implementasyonlarını hep birlikte yapacağız.

Docker ile “Keycloak” Kurulum

Bir komut satırı üzerinden Keycloak’ı aşağıdaki gibi başlatabilirsiniz:

docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin KEYCLOAK_IMAGE_PATH start-dev

Yukarıdaki işlemi yapabilmeniz için öncelikle docker kurulumu yapmış olmanız beklenmektedir. Ayrıca KEYCLOAK_IMAGE_PATH ile base olarak kullanılacak keycloak image’ının bulunduğu path’in verilmesi gerekiyor.

Yukarıdaki komut ile Keycloak server’ı, lokal port 8080 üzerinden ve yine komut içerisinden parametrik olarak verilen admin ve şifre bilgileri ile dışarıya açmış oluyoruz.

  • Keycloak Admin Console’a giderek ve daha önce komut içerisinde belirlediğimiz bilgilerle login olabiliriz:
  • Başarılı bir şekilde login olduktan sonra kendi projemiz için BlogAPIKeycloak adında yeni bir Realm oluşturuyoruz:
  • Realm oluşturduktan sonra sol menü’den Clients’a gidip, login-app adında yeni bir Client oluşturuyoruz:

Client oluşturma işlemini başarılı bir şekilde tamamladıktan sonra, client’ın detaylı ayarlarını yapabileceğimiz bir ekran gelecektir.

Client detay ayarlarını aşağıdaki gibi belirliyoruz:
- Client Protocol: openid-connect
- Access Type: confidential
-
Valid Redirect URIs: *

  • Client ayarları tamamlandıktan sonra blogapiuser adında yeni bir User oluşturuyoruz:
  • blogapiuser’ı oluşturduktan sonra Credentials sekmesinden ilgili user için password tanımlıyoruz ve temporary: off yapmayı unutmuyoruz:
  • Ayrıca ilgili user için Role Mappings sekmesinden default veya custom role atamaları yapabiliyoruz, offline validation için offline_access rolünü ekliyoruz:

Quickstart/Quickguide diyebileceğimiz Keycloak konfigurasyonlarını tamamlamış olduk, bu konfigurasyonlarla Production ortamına çıkılmaması gerektiğini de hatırlatalım.

Bütün bu yapılan konfigurasyonları ve Keycloak’ın bize sunduğu endpoint’leri görmek için aşağıdaki gibi curl komutu çalıştırılabilirsiniz:

Openid-Configuration

curl -X GET \ http://${host}:${port}/auth/realms/${realm}/.well-known/openid-configuration

curl -X GET \ http://localhost:8080/auth/realms/BlogAPIKeycloak/.well-known/openid-configuration

Yukarıdaki konfigurasyon sonucuna göre token_endpointini kullanarak access token üretebileceğimizi görüyoruz:

Generate Accces Token

”http://localhost:8080/realms/BlogAPIKeycloak/protocol/openid-connect/token”


curl \ -d "client_id=login-app" \ -d "client_secret=*********" \ -d "grant_type=password" \ -d "username=blogapiuser" \ -d "password=*****" \ "http://localhost:9080/auth/realms/myrealm/protocol/openid-connect/token"

Keycloak — Oracle

  • Keycloak default olarak postgresql ile uyumlu çalışacak şekilde tasarlanmıştır. Fakat biz Oracle ürünlerini kullandığımız ve desteğini aldığımız için özel bir pipeline tanımlayarak ve yine bazı özelleştirmeler yaparak Keycloak image’ımızı Oracle template ile konfigure edip, Keycloak ürünümüzün Oracle veritabanı ile çalışmasını sağladık.
  • İlerleyen zamanlarda bu fonksiyonaliteyi nasıl sağladığımızı yeni bir blog yazısıyla sizlerle paylaşabiliriz.

“Keycloak” ile Spring Boot API Entegrasyonu

  • Projenin veya modülün pom.xml dosyasına aşağıdaki bağımlılıkları ekliyoruz:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>17.0.0</version>
</dependency>
  • Bu dependency’leri ekledikten sonra projemizde bulunan bütün endpoint’ler auto configure özelliği sayesinde artık secure edilmiş oluyor. Daha sonra biz bu konfigurasyonu özelleştirmek istediğimiz için aşağıdaki SecurityConfig.java class’ını implemente ediyoruz:
@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter

@Autowired
private TokenVerifier tokenVerifier;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}

@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}

@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.addFilterBefore(new OfflineAuthenticationFilter(tokenVerifier), ForceEagerSessionCreationFilter.class)
.and()
.csrf().disable()
.authorizeRequests();
}

@PostConstruct
public void init(){
System.out.println("Security Config Constructed!");
}
}
  • Yukarıdaki özelleştirme sonucunda her bir request için öncelikle ilgili filtre çalışacak ve oradaki logic’e göre secure olup olmadığı kontrolü sağlanacak. Offline validation özelliğini kullanmak istediğimiz için aşağıdaki OfflineAuthenticationFilter.java class’ını implemente ediyoruz.

Offline Validation Filter

  • Gelen request header’dan Authorization header alınıp, doğru formatta olup olmadığı kontrol ediliyor. Eğer başarısız ise işlem burada kesilip, response olarak Unauthorized dönüyoruz. Bu kontrollerden sonra, ilgili token, header içerisinden elde ediliyor ve offline validation için verifyToken methoduna ilerletiliyor. Buradan dönen sonuç eğer olumsuz ise yine Unauthorized response, olumlu ise bir UsernamePasswordAuthenticationToken oluşturup, Spring Security’nin SecurityContext’ine ekleyerek filter chain’i devam ettiriyoruz.
public class OfflineAuthenticationFilter extends OncePerRequestFilter {

private TokenVerifier tokenVerifier;

public OfflineAuthenticationFilter(TokenVerifier tokenVerifier) {
this.tokenVerifier = tokenVerifier;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

String header = request.getHeader("Authorization");

if (header == null || !header.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return; // Eğer header doğru formatta değilse işlemi burada kes
}

String token = header.replace("Bearer ", "");
Claims claims = tokenVerifier.verifyToken(token); // Offline Validation

if (claims == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return; // Eğer claims doğru değilse işlemi burada kes
}

Map<String, Object> realmAccess = claims.get("realm_access", Map.class);
List<String> roles = (List<String>) realmAccess.get("roles");

List<GrantedAuthority> authorities = roles
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());

String preferredUsername = claims.get("preferred_username", String.class);

// Token doğrulanırsa, bir UsernamePasswordAuthenticationToken oluştur ve Spring Security'nin SecurityContext'ine ekle
SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(preferredUsername, token, authorities));


filterChain.doFilter(request, response);
}
}

Offline Validation — Token Verifier

  • Offline validation işleminin fonksiyonel olarak gerçekleştirilmesi için TokenVerifier.java class’ını aşağıdaki gibi implemente ediyoruz.
  • Ayrıca token verify etmek için pom.xml’e gerekli bağımlılıkları da ekliyoruz.
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
// KEYCLOAK_PUBLIC_KEY_STRING değeri aşağıdaki endpoint üzerinden alınabilir.

curl -X GET \ http://localhost:8080/auth/realms/BlogAPIKeycloak/
@Service
public class TokenVerifier {

public Claims verifyToken(String token) {
try {
String keycloakPublicKeyString = "KEYCLOAK_PUBLIC_KEY_STRING";
PublicKey publicKey = convertToPublicKey(keycloakPublicKeyString);

// AccessToken accessToken = verifier.getToken();
Jws<Claims> jws = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token);
Claims claims = jws.getBody();
return claims;
} catch (Exception e) {
return null;
}
}

private PublicKey convertToPublicKey(String base64PublicKey) throws Exception {
// Base64 kodlanmış anahtarı byte dizisine çevir
byte[] decodedKey = Base64.getDecoder().decode(base64PublicKey);

// Byte dizisini X509EncodedKeySpec nesnesine çevir
X509EncodedKeySpec spec = new X509EncodedKeySpec(decodedKey);

// KeyFactory oluştur ve PublicKey nesnesini al
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(spec);
}

}

Keycloak — Identity Management — Domain Topolojisi

En temelde, bir Spring Boot projesini secure hale getirip, bu proje içerisindeki endpoint’lere erişimi Security Configuration ile özelleştirerek ve bunu Keycloak ile entegre edip offline şekilde yapabilmek için gerekli adımları sizlerle paylaştık.

Akbank Teknoloji çatısı altında yapılan çalışmaları yakından takip edebilmek ve bu bilgi ağını genişletmek için takipte kalın!

Bu ürünün kurulumu ve entegrasyonu noktasında beraber çalıştığımız takım arkadaşım Ziya Orujaliyev’e katkılarından dolayı teşekkür ediyorum.

Referanslar:
https://www.keycloak.org/guides#getting-started
https://wjw465150.gitbooks.io/keycloak-documentation/content/securing_apps/
https://www.baeldung.com/spring-security-basic-authentication

--

--