spring security OIDC keycloak unable to get Authorities and Apply Authorization - spring-boot

I've Spring Security Oauth2 app, connected to OIDC server keycloak, I'm able to connect and get access token and perform authentication, however unable to perform Authorization.
The introspect of token responds with Authorities in json response as shown below.
{
"jti": "f21b1ecd-05b7-435b-a571-1b8554ae3666",
"exp": 1583995545,
"nbf": 0,
"iat": 1583994645,
"iss": "http://192.168.56.101:8080/auth/realms/master",
"sub": "e7462035-316e-4970-afde-e44ffd9f169e",
"typ": "Bearer",
"azp": "app1_client",
"auth_time": 1583994645,
"session_state": "7a36dc7f-dd5d-42cb-8684-398825fcacde",
"name": "Administrator 1",
"given_name": "Administrator",
"family_name": "1",
"preferred_username": "admin1",
"email_verified": false,
"acr": "1",
"resource_access": {
"app1_client": {
"roles": [
"APP1_ADMIN"
]
}
},
"scope": "email app1 profile",
"authorities": [
"ROLE_APP1_ADMIN"
],
"client_id": "app1_client",
"username": "admin1",
"active": true
}
However when I print Authorities in log I'm unable to get the Authorities ROLE_APP1_ADMIN instead in prints below log.
K-[ROLE_USER, SCOPE_address, SCOPE_app1, SCOPE_email, SCOPE_microprofile-jwt, SCOPE_offline_access, SCOPE_openid, SCOPE_phone, SCOPE_profile]
Below is HelloRest.java
#RestController
#Slf4j
#RequestMapping("/api")
public class HelloRest {
//#PreAuthorize("hasRole('APP1_ADMIN')")
#GetMapping("/admin")
public String admin(OAuth2AuthenticationToken e1) {
log.info("K-{}", e1.getAuthorities());
log.info("K-{}", e1.getAuthorizedClientRegistrationId());
log.info("K-{}", e1.getDetails());
log.info("K-{}", e1.getPrincipal().getAttributes());
log.info("K-{}", e1.getPrincipal().getAuthorities());
log.info("K-{}", e1.getName());
return "Hello from Admin of APP1";
}
#PreAuthorize("hasRole('APP1_USER')")
#GetMapping("/user")
public String user() {
return "Hello from User of APP1";
}
}
application.yml
server:
port: 8082
spring:
security:
oauth2:
# resourceserver:
# jwt:
# issuer-uri: http://192.168.56.101:8080/auth/realms/master
client:
provider:
keycloak:
issuer-uri: http://192.168.56.101:8080/auth/realms/master
registration:
keycloak:
client-id: app1_client
client-secret: <secret>
provider: keycloak
And finally 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.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.github.kprasad99</groupId>
<artifactId>app1-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>app1-backend-1</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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-security</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.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
How to apply Authorization using spring security with keycloak?

From source code it looks like, we need to write custom mapper, spring security by default adds scope as role and default Role ROLE_USER. Added below custom mapper.
#Component
#Slf4j
public class KGrantedAuthoritiesMapper implements GrantedAuthoritiesMapper {
#Override
public Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(mappedAuthorities::add);
authorities.forEach(authority -> {
if (OidcUserAuthority.class.isInstance(authority)) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();
Optional.ofNullable(userInfo.getClaimAsStringList("authorities")).orElse(Collections.emptyList())
.stream().map(SimpleGrantedAuthority::new).forEach(mappedAuthorities::add);
// Map the claims found in idToken and/or userInfo
// to one or more GrantedAuthority's and add it to mappedAuthorities
} else if (OAuth2UserAuthority.class.isInstance(authority)) {
OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority) authority;
Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();
log.info("{}", userAttributes);
// Map the attributes found in userAttributes
// to one or more GrantedAuthority's and add it to mappedAuthorities
// Not sure when this is being used
}
});
return mappedAuthorities;
}
}
Any configuration or in-built mappers available, please post, I will mark that as answer.

Related

thymeleaf extras security doesn't work with spring Security

SO as a beginner i have tried to make an ecommmerce website using spring boot 2.2.11 , spring security , thymeleaf and also json web token , My problem is when a user authentificate the template doesn't change even i put isAnonyms and IsAuthentificated tags of thymeleaf in my template.
I have two question here :
1-/ how to tell all controller that the user is already logged ?
2-/ how to pass the jwt token from the backend to frontend so that the user can make specific request ?
Here is my 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.11.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.webjars/bootstrap -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
<version>0.30</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version></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-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf</artifactId>
<version>3.0.11.RELEASE</version>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The part Of my Index.html that contains the thymeleaf tags:
<div class="forms ml-auto">
<a th:href="#{/login}" class="btn" sec:authorize="isAnonymous()"><span
class="fa fa-user-circle-o"></span> Sign In</a>
<a th:href="#{/signup}" class="btn" sec:authorize="isAnonymous()"><span
class="fa fa-pencil-square-o"></span> Sign Up</a>
<a th:href="#{/account}" class="btn" sec:authorize="isAuthenticated()"><span
class="fa fa-pencil-square-o"></span> Account</a>
<a th:href="#{/cart}" class="btn"> Cart <span> 0 </span> <i class="fa fa-shopping-cart"></i> </a>
<a th:href="#{/logout}" class="btn" sec:authorize="isAuthenticated()"><span
class="fa fa-user-circle-o"></span> Logout</a>
</div>
My Controller For Login :
#RequestMapping(value = "/login", method = RequestMethod.GET)
public String login(Model model) {
model.addAttribute("userDto",new UserDto());
return "signin";
}
#RequestMapping(value = "/login",method = RequestMethod.POST)
public String login(#ModelAttribute("userDto") #Valid UserDto userDto, BindingResult result , RedirectAttributes ra){
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userDto.getEmail(),userDto.getPassword()));
final UserDetails userDetails = userDetailsService.loadUserByUsername(userDto.getEmail());
if (!userDetails.getUsername().equalsIgnoreCase(userDto.getEmail()) ){
result.rejectValue("email",null,"Wrong Email");
}
if (!bCryptPasswordEncoder.matches(userDto.getPassword(),userDetails.getPassword())){
result.rejectValue("password","null","Wrong Password");
}
if (result.hasErrors()){
ra.addFlashAttribute("userDto",userDto);
return "signin";
}
final String jwt = jwtUtil.generateToken(userDetails);
System.out.println(jwt);
return "index";
}
My Spring Security Configuration :
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/resources/**", "/static/**", "/public/**").permitAll()
.antMatchers("/", "/signin/", "/signup","/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**")
.hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated().and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().formLogin().loginPage("/signin").defaultSuccessUrl("/")
.usernameParameter("email").passwordParameter("password")
.permitAll()
.defaultSuccessUrl("/",true)
.and().logout().logoutSuccessUrl("/")
.logoutRequestMatcher(new AntPathRequestMatcher("/home/logout"));
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
Before login :
img_before_login
Image After Login :
redirect to index
Login Successfully and switching to other page :
switch page
ps: I will be thankful for any solution or any advice .
You can get if the user is authenticated by specifying a Principal as method argument in the #Controller. If the value is null, then the request is not authenticated. Otherwise, request is authenticated.
#GetMapping("/foo")
String foo(Principal principal) {
boolean isAuthenticated = principal != null;
...
}
Often you would provide a JWT when authentication success is achieved. Here is an example application.
The first step is to provide a way to authenticate the user. In this instance, we validate a username/password with basic authentication.
#Configuration
public class RestConfig extends WebSecurityConfigurerAdapter {
#Value("${jwt.public.key}")
RSAPublicKey key;
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.authorizeRequests((authz) -> authz.anyRequest().authenticated())
.csrf((csrf) -> csrf.ignoringAntMatchers("/token"))
.httpBasic(Customizer.withDefaults())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
);
// #formatter:on
}
#Bean
UserDetailsService users() {
// #formatter:off
return new InMemoryUserDetailsManager(
User.withUsername("user")
.password("{noop}password")
.authorities("app")
.build()
);
// #formatter:on
}
#Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
}
}
Then after basic authentication is succeeds it reaches the controller which produces the successful JWT in the response:
#RestController
public class TokenController {
#Value("${jwt.private.key}")
RSAPrivateKey key;
#PostMapping("/token")
public String token(Authentication authentication) {
Instant now = Instant.now();
long expiry = 36000L;
// #formatter:off
String scope = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
JWTClaimsSet claims = new JWTClaimsSet.Builder()
.issuer("self")
.issueTime(new Date(now.toEpochMilli()))
.expirationTime(new Date(now.plusSeconds(expiry).toEpochMilli()))
.subject(authentication.getName())
.claim("scope", scope)
.build();
// #formatter:on
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256).build();
SignedJWT jwt = new SignedJWT(header, claims);
return sign(jwt).serialize();
}
SignedJWT sign(SignedJWT jwt) {
try {
jwt.sign(new RSASSASigner(this.key));
return jwt;
}
catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
}
NOTE: You didn't specifically ask, but the likely reason the Thymeleaf tags don't appear to be working is that you are in a stateless application, so the authentication is lost immediately after log in since the session is not created.

Error while creating dynamic cloud config using actuator

The controller is InfyGo_Booking to which #RefreshScope is added and also added required dependencies in pom.xml. I have also exposed the endpoints in bootstrap.properties. But still https://localhost:9000/actuator/refresh is giving me 404 error. Can you help me pointing out if I am missing something?
Controller:
#RestController
#RefreshScope
#RequestMapping("/book")
public class BookingController {
protected Logger logger = Logger.getLogger(BookingController.class.getName());
#Autowired
private TicketService ticketService;
#Autowired
private PassengerService passengerService;
private Ticket ticket;
private int noOfSeats;
ClientHttpRequestFactory requestFactory = new
HttpComponentsClientHttpRequestFactory(HttpClients.createDefault());
RestTemplate restTemplate = new RestTemplate(requestFactory);
//
public BookingController() {
ticket = new Ticket();
}
#PostMapping(value = "/{flightId}/{username}", produces = "application/json", consumes = "application/json")
public ResponseEntity<BookingDetails> bookFlight(#PathVariable("flightId") String flightId,
#Valid #RequestBody PassengerDetails passengerDetails, #PathVariable("username") String username,Errors errors) throws InfyGoServiceException, ARSServiceException {
if (errors.hasErrors()) {
return new ResponseEntity(new ClientErrorInformation(HttpStatus.BAD_REQUEST.value(),errors.getFieldError("passengerList").getDefaultMessage()), HttpStatus.BAD_REQUEST);
}
if(passengerDetails.getPassengerList().isEmpty())
throw new InfyGoServiceException(ExceptionConstants.PASSENGER_LIST_EMPTY.toString());
List<Passenger> passengerList = new ArrayList<Passenger>();
for (Passenger passengers : passengerDetails.getPassengerList()) {
passengerList.add(passengers);
}
System.out.println(passengerList.toString());
logger.log(Level.INFO, "Book Flight method ");
logger.log(Level.INFO, passengerDetails.toString());
int pnr = (int) (Math.random() * 1858955);
ticket.setPnr(pnr);
// Date date = new Date();
// Calendar calendar = Calendar.getInstance();
// calendar.setTime(date);
Flight flightDto= restTemplate.getForObject("http://localhost:9004/flights/"+flightId, Flight.class);
double fare= flightDto.getFare();
System.out.println("Fare per person:****** " + fare);
System.out.println("List size:****** " + passengerDetails.getPassengerList().size());
double totalFare = fare * (passengerDetails.getPassengerList().size());
BookingDetails bookingDetails = new BookingDetails();
bookingDetails.setPassengerList(passengerDetails.getPassengerList());
bookingDetails.setPnr(pnr);
bookingDetails.setTotalFare(totalFare);
ticket.setBookingDate(new Date());
System.out.println(ticket.getBookingDate());
ticket.setDepartureDate(flightDto.getFlightAvailableDate());
ticket.setDepartureTime(flightDto.getDepartureTime());
ticket.setFlightId(flightDto.getFlightId());
ticket.setUserId(username);
ticket.setTotalFare(totalFare);
noOfSeats = passengerDetails.getPassengerList().size();
ticket.setNoOfSeats(noOfSeats);
ticketService.createTicket(ticket);
addPassengers(bookingDetails.getPassengerList());
restTemplate.postForEntity("http://localhost:9004/flights/"+flightId+"/"+noOfSeats,null,null);
return new ResponseEntity<BookingDetails>(bookingDetails, HttpStatus.OK);
}
private void addPassengers(List<Passenger> passengers) {
for (Passenger passenger : passengers) {
passenger.setTicket(ticket);
}
passengerService.createPassenger(passengers);
}
}
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.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR5</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
bootstrap.properties:
spring.cloud.config.uri=http://localhost:1111
management.endpoints.web.exposure.include=*
It looks like you are trying to call the actuator as a URL from browser which by default uses GET method . Please try the same URL and hit a post request using any app , you must see a success response .

FF4J - Spring Boot - Custom Authorization Manager

I am trying to create a standalone feature flag server (centrally managed feature flag micro-service) backed by spring boot starters provided by FF4J. I was able to get it up and running with the web-console and REST API as well. I am now trying to just add the support of custom authorization manager as provided in the wiki, but based on the sample provided there, I am unclear as to how the authorization manager would be aware of the user context when it gets accessed from a different microservice which is implementing the feature. Below I have provided all the relevant code snippets. If you notice in CustomAuthorizationManager class, I have a currentUserThreadLocal variable, not sure how or who is going to set that at run time for FF4J to verify the user's role. Any help on this is really appreciated, as I having issues understanding how this works.
Also note, there is a toJson method in authorization manager that needs to be overridden, not sure what needs to go over there, any help with that is also appreciated.
Custom Authorization Manager
public class CustomAuthorizationManager implements AuthorizationsManager {
private static final Logger LOG = LoggerFactory.getLogger(FeatureFlagServerFeignTimeoutProperties.class);
private ThreadLocal<String> currentUserThreadLocal = new ThreadLocal<String>();
private List<UserRoleBean> userRoles;
#Autowired
private SecurityServiceFeignClient securityServiceFeignClient;
#PostConstruct
public void init() {
try {
userRoles = securityServiceFeignClient.fetchAllUserRoles();
} catch (Exception ex) {
LOG.error("Error while loading user roles", ex);
userRoles = new ArrayList<>();
}
}
#Override
public String getCurrentUserName() {
return currentUserThreadLocal.get();
}
#Override
public Set<String> getCurrentUserPermissions() {
String currentUser = getCurrentUserName();
Set<String> roles = new HashSet<>();
if (userRoles.size() != 0) {
roles = userRoles.stream().filter(userRole -> userRole.getUserLogin().equals(currentUser))
.map(userRole -> userRole.getRoleName()).collect(Collectors.toSet());
} else {
LOG.warn(
"No user roles available, check startup logs to check possible errors during loading of user roles, returning empty");
}
return roles;
}
#Override
public Set<String> listAllPermissions() {
Set<String> roles = new HashSet<>();
if (userRoles.size() != 0) {
roles = userRoles.stream().map(userRole -> userRole.getRoleName()).collect(Collectors.toSet());
} else {
LOG.warn(
"No user roles available, check startup logs to check possible errors during loading of user roles, returning empty");
}
return roles;
}
#Override
public String toJson() {
return null;
}
}
FF4J config
#Configuration
#ConditionalOnClass({ ConsoleServlet.class, FF4jDispatcherServlet.class })
public class Ff4jConfig extends SpringBootServletInitializer {
#Autowired
private DataSource dataSource;
#Bean
public ServletRegistrationBean<FF4jDispatcherServlet> ff4jDispatcherServletRegistrationBean(
FF4jDispatcherServlet ff4jDispatcherServlet) {
ServletRegistrationBean<FF4jDispatcherServlet> bean = new ServletRegistrationBean<FF4jDispatcherServlet>(
ff4jDispatcherServlet, "/feature-web-console/*");
bean.setName("ff4j-console");
bean.setLoadOnStartup(1);
return bean;
}
#Bean
#ConditionalOnMissingBean
public FF4jDispatcherServlet getFF4jDispatcherServlet() {
FF4jDispatcherServlet ff4jConsoleServlet = new FF4jDispatcherServlet();
ff4jConsoleServlet.setFf4j(getFF4j());
return ff4jConsoleServlet;
}
#Bean
public FF4j getFF4j() {
FF4j ff4j = new FF4j();
ff4j.setFeatureStore(new FeatureStoreSpringJdbc(dataSource));
ff4j.setPropertiesStore(new PropertyStoreSpringJdbc(dataSource));
ff4j.setEventRepository(new EventRepositorySpringJdbc(dataSource));
// Set authorization
CustomAuthorizationManager custAuthorizationManager = new CustomAuthorizationManager();
ff4j.setAuthorizationsManager(custAuthorizationManager);
// Enable audit mode
ff4j.audit(true);
return ff4j;
}
}
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>
<groupId>com.example</groupId>
<artifactId>feature-flag-server</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>feature-flag-server</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.RC2</spring-cloud.version>
<ff4j.version>1.8.2</ff4j.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<exclusions>
<!-- resolve swagger dependency issue - start -->
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
<!-- resolve swagger dependency issue - end -->
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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>
<!-- FF4J dependencies - start -->
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-spring-boot-starter</artifactId>
<version>${ff4j.version}</version>
</dependency>
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-store-springjdbc</artifactId>
<version>${ff4j.version}</version>
</dependency>
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-web</artifactId>
<version>${ff4j.version}</version>
</dependency>
<!-- FF4J dependencies - end -->
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Full disclosure I am the maintainer of the framework.
The documentation is not good on this part, improvements are in progress. But here is some explanation for a working project.
When using AuthorizationManager:
AuthorizationManager principle should be used only if you already enabled authentication in your application (LOGIN FORM, ROLES...). If not you can think about FlipStrategy to create your own predicates.
FF4j will rely on existing security frameworks to retrieve context of logged user, this is called the principal. As such this is unlikely for you to create your own custom implementation of AuthorizationManager except you are building your own authentication mechanism.
What to do:
You will use well known framework such as Spring Security of Apache Shiro to secure your applications and simply tell ff4j to rely on it.
How to do:
Here is working example using SPRING SECURITY:
https://github.com/ff4j/ff4j-samples/tree/master/spring-boot-2x/ff4j-sample-security-spring
Here is working example using APACHE SHIRO:
https://github.com/ff4j/ff4j-samples/tree/master/spring-boot-2x/ff4j-sample-security-shiro

Spring boot getting 401 unauthorized status code for simple get request

I am very new to Spring framework. I have created a new Spring Starter Project with following modules: web, mongo, security.
I have created a simple controller
#RestController
#RequestMapping("/users")
public class UserController {
private UserRepository userRepository;
#GetMapping("/all")
public List<User> getAllUsers(){
List<User> users = this.userRepository.findAll();
return users;
}
#PostMapping("/")
public void insert(#RequestBody User user){
this.userRepository.save(user);
}
}
And seeded some raw data to the database. When I make request to this route in Postman, I get the following response:
{
"timestamp": 1511113712858,
"status": 401,
"error": "Unauthorized",
"message": "Full authentication is required to access this resource",
"path": "/users/all"
}
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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ngt</groupId>
<artifactId>someArtifact</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>dermaskin</name>
<description>Demo project for Spring Boot with mongodb</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</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>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
What is causing the unauthorized response and how to disable it for the /all route? Thanks!
#Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers().antMatchers("/users/all").permitAll();
}
}
You need to configure Spring Security, by default all routes all secured for authrorization.
The code above disables security only for "/users/all" URL.
I was getting this error as I included
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
in my pom.xml file.
Just remove it and try again.
You can keep your security dependency but then you have to setup a userid and a password. This can be done by adding the following into your application.properties file located under
src/main/resources
folder
security.user.name=user # Default user name.
security.user.password= # your password here

Spring MongoDB authorization failed

I have an issue with Spring MongoDB authorization process - attempts to retrieve data via REST API led to response:
"error": "Internal Server Error",
"exception": "org.springframework.data.mongodb.CannotGetMongoDbConnectionException",
"message": "Failed to authenticate to database [testdb], username = [test_user], password = [t**t]"
I have installed and configured MongoDB. This is my MongoDB config file:
systemLog:
destination: file
logAppend: true
path: C:\Program Files\MongoDB\data\log\mongod.log
timeStampFormat: iso8601-utc
storage:
dbPath: C:\Program Files\MongoDB\data\db
journal:
enabled: true
processManagement:
# fork: true
pidFilePath: C:\Program Files\MongoDB\mongod.pid
net:
port: 27017
bindIp: 127.0.0.1
security:
authorization: enabled
Next step I have created admin user and user for testdb database:
> use admin
switched to db admin
> db.createUser({user: "admin", pwd: "qwerty", roles: ["root"]})
Successfully added user: { "user" : "admin", "roles" : [ "root" ] }
> use testdb
switched to db testdb
> db.createUser({user: "test_user", pwd: "test", roles: [{role: "readWrite", db: "testdb"}]})
Successfully added user: {
"user" : "test_user",
"roles" : [
{
"role" : "readWrite",
"db" : "testdb"
}
]
}
POM file:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.rest</groupId>
<artifactId>spring-rest-api</artifactId>
<version>1.0</version>
<packaging>war</packaging>
<properties>
<!-- Enable Java 8 -->
<java.version>1.8</java.version>
<guava.version>18.0</guava.version>
<hamcrest.version>1.3</hamcrest.version>
<mockito.version>1.9.5</mockito.version>
<assertj.version>1.7.0</assertj.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Configure the main class of our Spring Boot application -->
<start-class>org.wixanz.App</start-class>
</properties>
<!-- Inherit defaults from Spring Boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- Get the dependencies of a web application -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data MongoDB-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Util -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot Maven Support -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Java MongoDB configuration file:
#Configuration
#PropertySource("classpath:mongodb.properties")
public class MongoDBConfig {
#Autowired
Environment env;
#Bean
public MongoDbFactory mongoDbFactory() throws Exception {
UserCredentials userCredentials = new UserCredentials(env.getProperty("mongodb.username"), env.getProperty("mongodb.password"));
MongoClient mongo = new MongoClient(env.getProperty("mongodb.host"), Integer.parseInt(env.getProperty("mongodb.port")));
return new SimpleMongoDbFactory(mongo, env.getProperty("mongodb.db"), userCredentials);
}
#Bean
public MongoTemplate mongoTemplate() throws Exception {
return new MongoTemplate(mongoDbFactory());
}
}
MongoDB.properties file in project:
mongodb.host=localhost
mongodb.port=27017
mongodb.db=testdb
mongodb.username=test_user
mongodb.password=test
If I try to connect to testdb database without implementation of UserCredential than the connection established successfully and data received.
What I need to correct that database authorize connection passed successfully?
Connect to mongoDb
mongo 127.0.0.1:27017
Create User
Go to mongoDB console and delete your current user & set authSchema version to 3 instead of 5 ,
follow these commands in mongo console -
mongo use admin
db.system.users.remove({}) <== removing all users
db.system.version.remove({}) <== removing current version
db.system.version.insert({ "_id" : "authSchema", "currentVersion" : 3 })
Create User:
use test-dev-db
db.createUser(
{
user: "root",
pwd: "bnuy93JoLJjiop",
roles: [ "readWrite", "dbAdmin" ]
}
)

Resources