Wednesday, October 26, 2022

Quarkus Cassandra client CRUD

Quarkus A Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM, crafted from the best of breed Java libraries and standards.

Apache Cassandra is an open source NoSQL distributed database trusted by thousands of companies for scalability and high availability without compromising performance. Linear scalability and proven fault-tolerance on commodity hardware or cloud infrastructure make it the perfect platform for mission-critical data.


CREATING A QUARKUS APPLICATION












Integration Quarkus  with Cassandra.

  • Configuration Quarkus and Cassandra.
  • Define Datas models, Repository and Services.
  • Quarkus Resource.

Quarkus Rest  CRUD API 

MethodsUrls
GET/products
GET/products/{id}
POST/products
PUT/products/{id}
DELETE/products

Technology

  • Java 17
  • Quarkus 2.13.3.Final
  • Cassandra 4.0.7 
  • Maven 
  • Postman 
  • IntelliJ IDEA 
  • Docker

Project Structure


Configuration  Quarkus Spring Data PostgreSQL



pom.xml


<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.henry</groupId>
<artifactId>quarkus-cassandra-client</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<compiler-plugin.version>3.8.1</compiler-plugin.version>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>2.13.3.Final</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.0.0-M7</surefire-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-cassandra-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.quarkus/quarkus-resteasy-reactive-jackson -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>com.datastax.oss.quarkus</groupId>
<artifactId>cassandra-quarkus-client</artifactId>
</dependency>

<dependency>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-mapper-processor</artifactId>
<version>4.15.0</version>
</dependency>

<dependency>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-mapper-runtime</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<source>17</source> <!-- (or higher) -->
<target>17</target> <!-- (or higher) -->
<annotationProcessorPaths>
<path>
<groupId>com.datastax.oss</groupId>
<artifactId>java-driver-mapper-processor</artifactId>
<version>4.15.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<skipITs>false</skipITs>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
</profiles>
</project>

application.properties: We can configure multiple keyspace.


quarkus.http.port=9000
quarkus.cassandra.contact-points=localhost:9042
quarkus.cassandra.local-datacenter=datacenter1
keyspace.one=inventory


Data class model


package org.henry.model;

import com.datastax.oss.driver.api.mapper.annotations.Entity;
import com.datastax.oss.driver.api.mapper.annotations.PartitionKey;

import java.util.UUID;

@Entity
public class Product {

@PartitionKey private UUID id;
private String description;

public Product() {}

public Product(UUID id, String description) {
this.id = id;
this.description = description;
}

public UUID getId() { return id; }

public void setId(UUID id) { this.id = id; }

public String getDescription() { return description; }

public void setDescription(String description) { this.description = description; }
}

Repository Interface


package org.henry.repository;

import com.datastax.oss.driver.api.core.PagingIterable;
import com.datastax.oss.driver.api.mapper.annotations.*;
import org.henry.model.Product;

import java.util.UUID;

@Dao
public interface ProductDao {
@Select
Product findById(UUID productId);

@Select
PagingIterable<Product> findAll();

@Update
void update(Product product);

@Insert
void save(Product product);

@Delete
void delete(Product product);
}

Mapper interface

By official documentation: Mapper annotations are used to mark the interface, and indicate what kind of request each method should execute.  

Generating the code


The mapper uses annotation processing: it hooks into the Java compiler to analyze annotations, and generate additional classes that implement the mapping logic. Annotation processing is a common technique in modern frameworks, and is generally well supported by build tools and IDEs; this is covered in detail in Configuring the annotation processor.


Pay attention to the compiler output: the mapper processor will sometimes generate warnings if annotations are used incorrectly.


Using the generated code


One of the classes generated during annotation processing is InventoryMapperBuilder


package org.henry.configuration;

import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.core.CqlSession;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.henry.mapper.InventoryMapper;
import org.henry.mapper.InventoryMapperBuilder;
import org.henry.repository.ProductDao;

import javax.inject.Singleton;

@Singleton
public class Config {

/**
* Call here multiple keyspace with respective DAO
* */
@ConfigProperty(name = "keyspace.one")
String keyspace1;

public CqlSession session(){
CqlSession session = CqlSession.builder().build();
return session;
}

public ProductDao productDao(){
InventoryMapper inventoryMapper = new InventoryMapperBuilder(session()).build();
ProductDao dao = inventoryMapper.productDao(CqlIdentifier.fromCql(keyspace1));
return dao;
}
}

package org.henry.mapper;

import com.datastax.oss.driver.api.core.CqlIdentifier;
import com.datastax.oss.driver.api.mapper.annotations.DaoFactory;
import com.datastax.oss.driver.api.mapper.annotations.DaoKeyspace;
import com.datastax.oss.driver.api.mapper.annotations.Mapper;
import org.henry.repository.ProductDao;

@Mapper
public interface InventoryMapper {
@DaoFactory
ProductDao productDao(@DaoKeyspace CqlIdentifier keyspace);
}


Service Class


package org.henry.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.henry.configuration.Config;
import org.henry.model.Product;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@ApplicationScoped
public class ProductService {

public static JsonMapper mapper = new JsonMapper();

@Inject
Config config;

public void save(Product product) throws JsonProcessingException {
config.productDao().save(new Product(UUID.randomUUID(), product.getDescription()));
}

public void update(Product product, UUID productId) {
Optional<Product> res = Optional.ofNullable(config.productDao().findById(productId));
if (res.isPresent()) {
res.get().setDescription(product.getDescription());
config.productDao().update(res.get());
}
}

public Product findById(UUID productId) {
return config.productDao().findById(productId);
}

public List<Product> getAll() {
return config.productDao().findAll().all();
}

public void delete(Product product) {
config.productDao().delete(product);
}

}


Quarkus Rest APIs Resource


package org.henry.resource;


import com.fasterxml.jackson.core.JsonProcessingException;
import org.henry.model.Product;
import org.henry.service.ProductService;

import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.List;
import java.util.UUID;

@Path("/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {

@Inject
ProductService productService;

@POST
public void save(Product product) throws JsonProcessingException {
productService.save(product);
}

@PUT
@Path("{productId}")
public void update(Product product, UUID productId) {
productService.update(product,productId);
}

@GET
@Path("{productId}")
public Product findById(UUID productId) {
return productService.findById(productId);
}

@GET
public List<Product> getAll() {
return productService.getAll();
}

@DELETE
public void delete(Product product) {
productService.delete(product);
}


}


Set up Cassandra


docker pull cassandra

 docker run --name cassandra -p 127.0.0.1:9042:9042 -p 127.0.0.1:9160:9160   -d cassandra

 docker exec -it cassandra /bin/bash

 #cqlsh






Check datacenter name:


cqlsh> use system;
cqlsh:system> select data_center from local;

 data_center
-------------
 datacenter1

(1 rows)
cqlsh:system>


Created keyspaces: inventory.

CREATE KEYSPACE inventory WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}; CREATE TABLE inventory.product(id uuid PRIMARY KEY, description text);


Run & Test


Run Quarkus application with next maven command : 
./mvnw compile quarkus:dev or ./mvnw test (Run test class) by console or IntelliJ.

POST.
http://localhost:9000/products

















PUT.
http://localhost:9000/products/{id}



















DELETE.
http://localhost:9000/products


















GET.
http://localhost:9000/products



















GET.
http://localhost:9000/products/{id}



















Source Code


Here on Github.





References.

https://quarkus.io/guides/cassandra
https://quarkus.io/guides/cdi-reference
https://quarkus.io/guides/config
https://docs.datastax.com/en/developer/java-driver/4.2/manual/mapper/
https://docs.datastax.com/en/developer/java-driver/4.2/manual/mapper/config/
https://docs.datastax.com/en/developer/java-driver/3.0/
https://cassandra.apache.org/_/index.html























Thursday, October 6, 2022

Microservices Security Patterns and Protocols with Spring Security


Overview:

  • OAuth2.0 Authorization Framework, Spring boot 2.5.2 and Java 11 (Deprecated)
  • JSON Web Tokens , Spring boot 2.4.4 and Kotlin 1.7.10,  (Deprecated)
  • OpenID Connect
  • SAML V2.0 

OAuth2.0 Authorization Framework

OAuth 2.0 is the industry-standard protocol for authorization. OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices. 

OAuth2.0 Roles

  • Resource Owner 
    • The user or system that owns the data that is to be shared.
  • Resource Server  
    • A server that protects the user’s resources is the server hosting the resources. for example Facebook or Google is a resource server.
  • Client Application
    •  The client application that requires access to the protected resources. 
  • Authorization Server
    • Is the server authorizing the client app to access the resources of the resource owner.
These roles are illustrated in this diagram:




For implementing code it used the authorization code. The authorization code flow offers a few benefits over the other grant types. When the user authorizes the application, they are redirected back to the application with a temporary code in the URL. 

Creating a Spring Boot Application

In this examples we're integration Spring Boot, OAuth2 and Ldap .ldif file.

  • Configuration Spring boot,  Authorization Server,  Resource Server 
  • Configuration Spring boot,  Client Application.

LDAP 

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

Authorization Server and Resource Server configuration 

Technology

  • Spring Boot 2.5.2
  • OAuth2
  • Java 11
  • Maven
  • IntelliJ IDEA 

Project Structure




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.5.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.henry</groupId>
<artifactId>auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>auth</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-jndi</artifactId>
<version>1.5.5</version>
</dependency>


<!-- Spring LDAP -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
</dependency>


<!-- API, java.xml.bind module -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.2</version>
</dependency>

<!-- Runtime, com.sun.xml.bind module -->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.2</version>
</dependency>

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

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.yml


server:
port: 8081
servlet:
context-path: /auth

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

client:
id: henry
secret: secret
redirect: http://localhost:8082/ui/index


Configuration package.

package com.henry.configuration;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
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.AuthorizationServerSecurityConfigurer;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

@Value("${client.id}")
private String clientId;

@Value("${client.secret}")
private String clientSecret;

@Value("${client.redirect}")
private String uriRedirect;


private final BCryptPasswordEncoder passwordEncoder;

public AuthServerConfig(BCryptPasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}

@Override
public void configure(
AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}



@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(clientId)
.secret(passwordEncoder.encode(clientSecret))
.authorizedGrantTypes("authorization_code")
.scopes("user_info")
.autoApprove(true).redirectUris(uriRedirect);
}

}

package com.henry.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/login", "/oauth/authorize")
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and()
.logout()
.permitAll();
;
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/* auth.inMemoryAuthentication()
.withUser("henry")
.password(passwordEncoder().encode("123"))
.roles("USER");*/
auth
.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.contextSource()
.url("ldap://localhost:8389/dc=springframework,dc=org")
.and()
.passwordCompare()
.passwordEncoder(new BCryptPasswordEncoder())
.passwordAttribute("userPassword");
}

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

Controller

package com.henry.controller;

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@EnableResourceServer
@RestController
@RequestMapping("/rest/hello")
public class HelloResource {

@GetMapping("/principal")
public Principal user(Principal principal) {
return principal;
}

@GetMapping
public String hello(Principal principal) {
return "Hello "+principal.getName();
}

}

 

Client Application configuration 

Technology

  • Spring Boot 2.5.2
  • OAuth2
  • Java 11
  • Maven
  • IntelliJ IDEA 

Project Structure





















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.5.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.henry</groupId>
<artifactId>authClient</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>authClient</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

application.yml


server:
port: 8082
servlet:
context-path: /ui

spring:
security:
basic:
enabled: false
oauth2:
client:
registration:
auth:
client-id: henry
client-secret: secret
client-authentication-method: basic
scope: user_info
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8082/ui/index
provider:
auth:
authorization-uri: http://localhost:8081/auth/oauth/authorize
token-uri: http://localhost:8081/auth/oauth/token
user-name-attribute: preferred_username
thymeleaf:
cache: false


Configuration package.

package com.henry.configuration;


import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;

@EnableWebSecurity
public class UiSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
public void configure(HttpSecurity http) throws Exception {


http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/**", "/login**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.logout()
.logoutUrl("/logout")
.and()
.oauth2Login()
;


}

@Bean
WebClient webClient(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepository,
authorizedClientRepository);
oauth2.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder().apply(oauth2.oauth2Configuration()).build();
}
}

Controller

package com.henry.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {


@GetMapping("/")
public String home1() {
System.out.println("redirect OAuth2 Login");
return "redirect:/oauth2/authorization/auth";
}

@GetMapping("/index")
public String getLoginPage() {
return "index";
}

}

Run & Test



Internally to make this:

https://YOUR_DOMAIN/authorize?
    response_type=code&
    client_id=YOUR_CLIENT_ID&
    redirect_uri=https://YOUR_APP/callback&
    scope=SCOPE&
    state=STATE


Redirect to:

Ldap user :
Username: henry
Password: 123













And back to client, where we get the authorization code for generated Access Token for access API.





Get the code from URL and then we can generated the Access Token, this way:

curl --request POST \
  --url 'https://YOUR_DOMAIN/oauth/token' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data grant_type=authorization_code \
  --data 'client_id=YOUR_CLIENT_ID' \
  --data client_secret=YOUR_CLIENT_SECRET \
  --data code=YOUR_AUTHORIZATION_CODE \
  --data 'redirect_uri=https://YOUR_APP/callback'


POST: http://localhost:8081/auth/oauth/token

If you want to test change the code.
















With Access Token we can access for API.

GET: http://localhost:8081/auth/rest/hello?access_token=your_token











JSON Web Tokens

JSON Web Token JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.

JSON Web Token structure

JSON Web Tokens consist of three parts separated by dots (.), which are:

  • Header
  • Payload
  • Signature

Diagram:



For implementing code it used the OAuth2 with JWT.

Creating a Spring Boot Application

In this example we're integration Spring Boot, Kotlin.

  • Configuration Spring boot, Kotlin, Authorization Server,  Resource Server and Spring Security.
  • Spring Controllers  

LDAP 

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


Authorization Server and Resource Server configuration 

Technology

  • Spring Boot 2.4.4
  • Kotlin 1.7.10
  • OAuth2
  • JWT
  • Java 11
  • Maven
  • IntelliJ IDEA 

Project Structure





















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.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hxiloj</groupId>
<artifactId>SpringBoot2-oauth2-Ldap</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringBoot2-oauth2-Ldap</name>
<description>SpringBoot2-oauth2-Ldap</description>

<properties>
<java.version>11</java.version>
<kotlin.version>1.7.10</kotlin.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
</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>net.sourceforge.collections</groupId>
<artifactId>collections-generic</artifactId>
<version>4.01</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<!-- API, java.xml.bind module -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>

<!-- Runtime, com.sun.xml.bind module -->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<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>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<finalName>auth</finalName>
</build>

</project>

application.yml


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

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

client:
id: henry
secret: secret

Configuration package.

package com.hxiloj.configuration

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
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
open class AuthorizationServerConfig : AuthorizationServerConfigurerAdapter() {


@Value("\${client.id}")
private val clientId: String? = null

@Value("\${client.secret}")
private val clientSecret: String? = null

@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(clientId)
.secret(passwordEncoder!!.encode(clientSecret))
.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
open fun tokenStore(): JwtTokenStore {
return JwtTokenStore(accessTokenConverter())
}

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


package com.hxiloj.configuration

import org.springframework.boot.web.servlet.FilterRegistrationBean

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.Ordered
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
import org.springframework.web.filter.CorsFilter
import java.util.*


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

@Bean
open 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
}

@Bean
open fun corsFilter(): FilterRegistrationBean<CorsFilter> {
val bean = FilterRegistrationBean(
CorsFilter(corsConfigurationSource())
)
bean.order = Ordered.HIGHEST_PRECEDENCE
return bean
}
}

package com.hxiloj.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)
open 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
open 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)
open fun approvalStore(tokenStore: TokenStore?): ApprovalStore {
val store = TokenApprovalStore()
store.setTokenStore(tokenStore)
return store
}

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

Controller

package com.hxiloj.controller

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


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

@GetMapping
fun hello(principal: Principal): String? {
return "Hello " + principal.name
}

}



Run & Test


Ldap user :
Username: henry
Password: 123

POSTMAN


Get access token

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

Type: Basic Auth
Username: henry
Password: secret



































GET: headers with  

Key: Authorization 
value:  Bearer {token}

http://localhost:9000/rest/hello












OpenID Connect

OpenID Connect is a simple identity layer on top of the OAuth 2.0 protocol. It allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server, as well as to obtain basic profile information about the End-User in an interoperable and REST-like manner.

OpenID Connect allows clients of all types, including Web-based, mobile, and JavaScript clients, to request and receive information about authenticated sessions and end-users. The specification suite is extensible, allowing participants to use optional features such as encryption of identity data, discovery of OpenID Providers, and logout, when it makes sense for them. in OpenID Connect, OAuth 2.0 capabilities are integrated with the protocol itself.



                                                                                                  [Image]. https://developers.redhat.com


For implementing code it used OpenId Connect, JWT and OAuth2. however for implementing it is a little a bit extensive for that I posted two examples:



SAML V2.0 

SAML version 2.0 was approved as an OASIS Standard in March 2005.
Approved Errata for SAML V2.0 was last produced by the SSTC on 1 May 2012. In addition to the normative errata document, the following non-normative "errata composite" documents have been provided that combine the prescribed corrections with the original specification text, illustrating the changes with margin change bars, struck-through original text, and highlighted new text.

What is SAML?

Security Assertion Markup Language. It is an XML-based open-standard for transferring identity data between two parties: an identity provider (IdP) and a service provider (SP).


                                                                                                [Image]. SpringOne Platform


For implementing code, I created one post, so I think SAML 2.0 is lit a bit more complex than OpenId Connect, JWT and OAuth2. 





Source Code


OAuth2.0 Authorization Framework

Here on Github.


JSON Web Tokens

Here on Github.


OpenID Connect 

GitHub Repositories:

SAML 2.0 

Here on  Github.


References:

https://oauth.net/2/
https://auth0.com/intro-to-iam/what-is-oauth-2/
https://auth0.com/docs/get-started/authentication-and-authorization-flow/add-login-auth-code-flow
https://jwt.io/introduction
https://developers.onelogin.com/openid-connect
https://jenkov.com/tutorials/oauth2/roles.html






















Multiple Data Sources in Spring Boot 3 with Java 21

  In this blog post, we'll explore the configuration and setup for a Spring Boot 3 application with Java 21 that uses multiple data sour...