Friday, July 29, 2022

Spring Boot 2.4.4, Kotlin 1.6, Basic Auth, JWT and LDAP


In this post. I will share a basic example,  how to integrate Spring Boot , Kotlin , Basic Auth, JWT and LDAP. However since Spring Security 5 the oauth2 is deprecated so I suggest to use Open Source Identity, Keycloak, OpenIAM, CAS  etc. Or We will wait for new version Spring 6 in Nov 2022  this version will come available Spring Authorization Server 1.0

Basic Auth 

Basic Authentication is a Method HTTP where we provide the username and password to the people making requests.

JSON Web Token

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

LDAP 

I this example used test-server.ldif LDIF  file provide by Spring.


Creating a Spring Boot Application


In this examples we're integration Spring Boot, Kotlin, Basic Auth and Ldap .ldif file.


Spring Boot Rest  API 

This is API

Methods URLs
POST /oauth/token
GET /principal
GET /users

Technology

  • Spring Boot 2.4.4
  • Kotlin 1.6.21
  • JWT
  • OAuth2
  • Java 17 (Zulu)
  • Maven 
  • IntelliJ IDEA 
  • test-server.ldif

Project Structure





















Configuration Spring Boot project 


pom.xml


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.henry</groupId>
<artifactId>SpringBoot2KotlinBasicAuthJWTLdap</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringBoot2KotlinBasicAuthJWTLdap</name>
<description>Spring Boot 2 + Kotlin + Basic Auth + JWT + LDAP</description>
<properties>
<java.version>17</java.version>
<kotlin.version>1.6.21</kotlin.version>
</properties>
<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.ldap/spring-ldap-core -->
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
<version>2.4.1</version>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
</dependency>
<!-- oauth2 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-jndi</artifactId>
<version>1.5.5</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<!--<scope>provided</scope>-->
</dependency>

<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.4.0-b180830.0359</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.1</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

</project>


application.yml


server:
port: 9000
servlet:
context-path: /

spring:
ldap:
embedded:
base-dn: dc=springframework,dc=org
ldif: classpath:test-server.ldif
port: 8389
url: ldap://localhost:8389/


Package Security configuration


AuthorizationServerConfig.java 

client: henry and secret: secret and authorization grant password and refresh_token, here is official documentation provided by Spring.

package com.henry.configuration

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore


@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig : AuthorizationServerConfigurerAdapter() {
@Autowired
@Qualifier("authenticationManagerBean")
private val authenticationManager: AuthenticationManager? = null

@Autowired
private val passwordEncoder: BCryptPasswordEncoder? = null

@Throws(Exception::class)
override fun configure(security: AuthorizationServerSecurityConfigurer) {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
}

@Throws(Exception::class)
override fun configure(clients: ClientDetailsServiceConfigurer) {
clients.inMemory().withClient("henry")
.secret(passwordEncoder!!.encode("secret"))
.scopes("read", "write")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(3600)
.accessTokenValiditySeconds(3600)
}

@Throws(Exception::class)
override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) {
endpoints.authenticationManager(authenticationManager)
.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
}

@Bean
fun tokenStore(): JwtTokenStore {
return JwtTokenStore(accessTokenConverter())
}

@Bean
fun accessTokenConverter(): JwtAccessTokenConverter {
val converter = JwtAccessTokenConverter()
converter.setSigningKey("123")
return converter
}
}

ResourceServerConfig.java 

Configurate the cors configuration and more details here official documentation provided by Spring.

package com.henry.configuration

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource


@Configuration
@EnableResourceServer
class ResourceServerConfig : ResourceServerConfigurerAdapter() {
@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.cors().and().authorizeRequests().antMatchers("/webjars/**").permitAll().anyRequest().authenticated().and().cors()
.configurationSource(corsConfigurationSource())
}

@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val config = CorsConfiguration()
config.allowedOrigins = listOf("*")
config.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
config.allowCredentials = true
config.allowedHeaders = listOf("Content-Type", "Authorization")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", config)
return source
}


}

SpringSecurityConfig.java

Configurate LDAP ldif file provided by Spring and stateless.

package com.henry.configuration

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.oauth2.provider.ClientDetailsService
import org.springframework.security.oauth2.provider.approval.ApprovalStore
import org.springframework.security.oauth2.provider.approval.TokenApprovalStore
import org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory
import org.springframework.security.oauth2.provider.token.TokenStore


@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
class SpringSecurityConfig : WebSecurityConfigurerAdapter() {
@Autowired
private val clientDetailsService: ClientDetailsService? = null

@Bean
@Throws(Exception::class)
override fun authenticationManagerBean(): AuthenticationManager {
return super.authenticationManagerBean()
}

@Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
auth
.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.contextSource()
.url("ldap://localhost:8389/dc=springframework,dc=org")
.and()
.passwordCompare()
.passwordEncoder(BCryptPasswordEncoder())
.passwordAttribute("userPassword")
}

@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.csrf().disable()
.cors().disable()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest().authenticated()
.and().httpBasic()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}

@Bean
@Autowired
fun userApprovalHandler(tokenStore: TokenStore?): TokenStoreUserApprovalHandler {
val handler = TokenStoreUserApprovalHandler()
handler.setTokenStore(tokenStore)
handler.setRequestFactory(DefaultOAuth2RequestFactory(clientDetailsService))
handler.setClientDetailsService(clientDetailsService)
return handler
}

@Bean
@Autowired
@Throws(Exception::class)
fun approvalStore(tokenStore: TokenStore?): ApprovalStore {
val store = TokenApprovalStore()
store.setTokenStore(tokenStore)
return store
}

@Bean
fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder()
}
}

Spring Rest APIs Controller


package com.henry.controller

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import java.security.Principal
import java.util.*


@RestController
class DefaultRestController {
@GetMapping("/principal")
fun user(principal: Principal): Principal {
return principal
}

@GetMapping("/users")
fun hello(principal: Principal): Map<String, String>? {
return Collections.singletonMap("response", "Hello ${principal.name}")
}

}

Run & Test


Run Spring Boot application with command: mvn spring-boot:run. by console, IntelliJ etc.

Credentials for test:

Basic auth:
client: henry
secret: secret

LDAP by ldif file:
user: henry
password: 123


POSTMAN

Type: Basic Auth
Username: henry
Password: secret













Body: x-www-form-urlencoded
username: henry
password: 123
grant_type: password











Get access token

POST: http://localhost:9000/oauth/token


















When we have an access token then just add headers with  Authorization Bearer {token}

GET:

http://localhost:9000/principal

















GET:

http://localhost:9000/users

















Source Code


Here on GitHub.





References.

https://www.baeldung.com/keycloak-embedded-in-spring-boot-app
https://www.baeldung.com/spring-security-oauth-jwt
https://jwt.io/introduction
https://squareball.co/blog/the-difference-between-basic-auth-and-oauth
https://www.twilio.com/docs/glossary/what-is-basic-authentication











No comments:

Post a Comment

Virtual Threads in Java 21: Simplified Concurrency for Modern Applications

  With Java 21, Virtual Threads have redefined how we approach concurrency, offering a lightweight and efficient way to handle parallel and ...