Skip to content

Commit

Permalink
upgrade to Spring Boot 3
Browse files Browse the repository at this point in the history
since Keycloak does no longer support the Spring Boot Starter we may just as
well upgrade and do the integration manually

Signed-off-by: Kai Helbig <[email protected]>
  • Loading branch information
ostrya committed Jun 7, 2024
1 parent 09feae0 commit a3a6b41
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 48 deletions.
6 changes: 3 additions & 3 deletions example-backend/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
buildscript {
ext {
// spring boot 3 is not (yet) supported by the Keycloak spring boot starter
springBootVersion = '2.7.16'
springBootVersion = '3.3.0'
}
repositories {
mavenCentral()
Expand Down Expand Up @@ -29,8 +28,9 @@ dependencies {

implementation "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-resource-server:$springBootVersion"
implementation "org.springframework.boot:spring-boot-configuration-processor:$springBootVersion"
implementation "org.keycloak:keycloak-spring-boot-starter:$keycloak_version"
implementation "org.keycloak:keycloak-policy-enforcer:$keycloak_version"
testImplementation project(':mock-junit')
testImplementation project(':mock-junit5')
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
package com.tngtech.keycloakmock.examplebackend;

import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@SpringBootApplication
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@EnableMethodSecurity(securedEnabled = true)
@ConfigurationPropertiesScan("com.tngtech.keycloakmock.examplebackend.config")
public class ExampleBackendApplication {

public static void main(String[] args) {
SpringApplication.run(ExampleBackendApplication.class, args);
}

@Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}

@Bean
public GrantedAuthorityDefaults grantedAuthorityDefaults() {
// Remove the default ROLE_ prefix that spring boot otherwise expects
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.tngtech.keycloakmock.examplebackend.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;

@ConfigurationProperties(prefix = "keycloak")
public class KeycloakConfiguration {
private final String clientId;

@ConstructorBinding
public KeycloakConfiguration(String clientId) {
this.clientId = clientId;
}

public String getClientId() {
return clientId;
}
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,62 @@
package com.tngtech.keycloakmock.examplebackend.config;

import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.web.SecurityFilterChain;

@KeycloakConfiguration
public class WebSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Configuration
public class WebSecurityConfig {
private final KeycloakConfiguration keycloakConfiguration;

@Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.antMatchers("/api/**")
.authenticated()
.antMatchers("/**")
.permitAll();
public WebSecurityConfig(KeycloakConfiguration keycloakConfiguration) {
this.keycloakConfiguration = keycloakConfiguration;
}

@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
@Bean
public SecurityFilterChain configure(final HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
authorizeRequests ->
authorizeRequests
.requestMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.requestMatchers("/api/**")
.authenticated()
.requestMatchers("/**")
.permitAll())
.oauth2ResourceServer(
server -> server.jwt(jwt -> jwt.jwtAuthenticationConverter(this::convert)));
return http.build();
}

@Autowired
public void configureGlobal(final AuthenticationManagerBuilder auth) {
auth.authenticationProvider(keycloakAuthenticationProvider());
// as Keycloak no longer supports their Spring Boot Starter module, we need to extract roles
// ourselves here ...
private AbstractAuthenticationToken convert(Jwt jwt) {
Set<GrantedAuthority> authorities =
Stream.concat(
jwt
.<Map<String, Collection<String>>>getClaim("realm_access")
.getOrDefault("roles", Collections.emptyList())
.stream(),
jwt
.<Map<String, Map<String, Collection<String>>>>getClaim("resource_access")
.getOrDefault(keycloakConfiguration.getClientId(), Collections.emptyMap())
.getOrDefault("roles", Collections.emptyList())
.stream())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.tngtech.keycloakmock.examplebackend.rest;

import java.security.Principal;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -10,7 +10,7 @@
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
public String hello(@AuthenticationPrincipal Principal user) {
return "Hello " + user.getName();
public String hello(@AuthenticationPrincipal Jwt user) {
return "Hello " + user.getSubject();
}
}
16 changes: 9 additions & 7 deletions example-backend/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ server:
port: 8080

keycloak:
realm: realm
bearer-only: true
auth-server-url: http://localhost:8000/auth
resource: client
principal-attribute: preferred_username
# default has changed to 'external', and within e2e tests the docker IP is not recognized as internal
ssl-required: none
client-id: server

spring:
security:
oauth2:
resourceserver:
jwt:
audiences: [ "${keycloak.client-id}"]
issuer-uri: "http://localhost:8000/auth/realms/realm"
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
Expand Down Expand Up @@ -76,4 +76,18 @@ public void authentication_with_role_works() {
.and()
.body(equalTo("you may feel very special here"));
}

@Test
public void authentication_with_resource_role_works() {
RestAssured.given()
.auth()
.preemptive()
.oauth2(mock.getAccessToken(aTokenConfig().withResourceRole("server", "vip").build()))
.when()
.get("/api/vip")
.then()
.statusCode(200)
.and()
.body(equalTo("you may feel very special here"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.boot.test.web.server.LocalServerPort;

@SpringBootTest(
classes = ExampleBackendApplication.class,
Expand Down Expand Up @@ -61,7 +61,7 @@ void authentication_without_role_fails() {
}

@Test
void authentication_with_role_works() {
void authentication_with_realm_role_works() {
RestAssured.given()
.auth()
.preemptive()
Expand All @@ -73,4 +73,18 @@ void authentication_with_role_works() {
.and()
.body(equalTo("you may feel very special here"));
}

@Test
void authentication_with_resource_role_works() {
RestAssured.given()
.auth()
.preemptive()
.oauth2(mock.getAccessToken(aTokenConfig().withResourceRole("server", "vip").build()))
.when()
.get("/api/vip")
.then()
.statusCode(200)
.and()
.body(equalTo("you may feel very special here"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
public class LogoutRoute implements Handler<RoutingContext> {

private static final String LEGACY_REDIRECT_URI = "redirect_uri";

/**
* <a href="https://github.com/keycloak/keycloak/pull/10887/">Before Keycloak 18</a>, the logout
* endpoint had used the {@value #LEGACY_REDIRECT_URI} query parameter.
Expand Down

0 comments on commit a3a6b41

Please sign in to comment.