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

Deploying a Spring Boot Application with Cloud SQL and Cloud Run on GCP

In this post, we'll explore how to provision Cloud SQL instances with different connectivity options using Terraform and then access the...