JAVA-18113 Update spring-boot-swagger-keycloak module (#13506)

* JAVA-18113 Update spring-boot-swagger-keycloak module
This commit is contained in:
anuragkumawat 2023-05-03 14:28:43 +05:30 committed by GitHub
parent 5cac883023
commit f5910afe6f
11 changed files with 144 additions and 324 deletions

View File

@ -1,75 +1,72 @@
<?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>
<artifactId>spring-boot-swagger-keycloak</artifactId>
<version>0.1.0-SNAPSHOT</version>
<name>spring-boot-swagger-keycloak</name>
<packaging>jar</packaging>
<description>Module For Spring Boot Swagger UI with Keycloak</description>
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>
<artifactId>spring-boot-swagger-keycloak</artifactId>
<version>0.1.0-SNAPSHOT</version>
<name>spring-boot-swagger-keycloak</name>
<packaging>jar</packaging>
<description>Module For Spring Boot Swagger UI with Keycloak</description>
<parent>
<groupId>com.baeldung.spring-boot-modules</groupId>
<artifactId>spring-boot-modules</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-3</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../parent-boot-3</relativePath>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>${keycloak.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-bom</artifactId>
<version>${log4j2.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-bom</artifactId>
<version>${log4j2.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${springfox.version}</version>
</dependency>
<!-- Authentication with with Keycloak -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
<!-- Authorization with MethodSecurity (@Secured) - optional -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<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-resource-server</artifactId>
</dependency>
<!-- Authorization with MethodSecurity (@Secured) - optional -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>${javax.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<properties>
<spring-boot.version>2.4.5</spring-boot.version>
<springfox.version>3.0.0</springfox.version>
<keycloak.version>15.0.2</keycloak.version>
<log4j2.version>2.17.1</log4j2.version>
</properties>
<properties>
<springdoc.version>2.1.0</springdoc.version>
<log4j2.version>2.17.1</log4j2.version>
<javax.version>1.3.2</javax.version>
</properties>
</project>

View File

@ -1,59 +1,40 @@
package com.baeldung.swaggerkeycloak;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
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.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
@KeycloakConfiguration
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class GlobalSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
public class GlobalSecurityConfig {
@Override
@Bean
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
// otherwise, we'll get an error 'permitAll only works with HttpSecurity.authorizeRequests()'
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.csrf().disable()
.authorizeRequests()
// we can set up authorization here alternatively to @Secured methods
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/api/**").authenticated()
// force authentication for all requests (and use global method security)
.anyRequest().permitAll();
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests()
.requestMatchers(HttpMethod.OPTIONS)
.permitAll()
.requestMatchers("/api/**")
.authenticated()
.anyRequest()
.permitAll();
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
/*
* re-configure Spring Security to use
* registers the KeycloakAuthenticationProvider with the authentication manager
*/
@Autowired
void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider provider = keycloakAuthenticationProvider();
provider.setGrantedAuthoritiesMapper(authoritiesMapper());
auth.authenticationProvider(provider);
}
GrantedAuthoritiesMapper authoritiesMapper() {
SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();
mapper.setPrefix("ROLE_"); // Spring Security adds a prefix to the authority/role names (we use the default here)
mapper.setConvertToUpperCase(true); // convert names to uppercase
mapper.setDefaultAuthority("ROLE_ANONYMOUS"); // set a default authority
return mapper;
}
}
}

View File

@ -1,23 +0,0 @@
package com.baeldung.swaggerkeycloak;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class KeycloakConfigResolverConfig {
/*
* re-configure keycloak adapter for Spring Boot environment,
* i.e. to read config from application.yml
* (otherwise, we need a keycloak.json file)
*/
@Bean
public KeycloakConfigResolver configResolver() {
return new KeycloakSpringBootConfigResolver();
}
}

View File

@ -1,22 +1,17 @@
package com.baeldung.swaggerkeycloak;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import springfox.documentation.builders.OAuth2SchemeBuilder;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.SecurityConfiguration;
import springfox.documentation.swagger.web.SecurityConfigurationBuilder;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.OAuthFlow;
import io.swagger.v3.oas.models.security.OAuthFlows;
import io.swagger.v3.oas.models.security.Scopes;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
@Configuration
public class OpenAPISecurityConfig {
@ -25,59 +20,35 @@ public class OpenAPISecurityConfig {
String authServerUrl;
@Value("${keycloak.realm}")
String realm;
@Value("${keycloak.resource}")
private String clientId;
@Value("${keycloak.credentials.secret}")
private String clientSecret;
@Autowired
void addSecurity(Docket docket) {
docket
.securitySchemes(Collections.singletonList(authenticationScheme()))
.securityContexts(Collections.singletonList(securityContext()));
}
private SecurityScheme authenticationScheme() {
return new OAuth2SchemeBuilder("implicit")
.name("my_oAuth_security_schema")
.authorizationUrl(authServerUrl + "/realms/" + realm)
.scopes(authorizationScopes())
.build();
}
private List<AuthorizationScope> authorizationScopes() {
return Arrays.asList(
new AuthorizationScope("read_access", "read data"),
new AuthorizationScope("write_access", "modify data")
);
}
private SecurityContext securityContext() {
return SecurityContext.
builder().
securityReferences(readAccessAuth())
.operationSelector(operationContext -> HttpMethod.GET.equals(operationContext.httpMethod()))
.build();
}
private List<SecurityReference> readAccessAuth() {
AuthorizationScope[] authorizationScopes = new AuthorizationScope[] { authorizationScopes().get(0) };
return Collections.singletonList(
new SecurityReference("my_oAuth_security_schema", authorizationScopes)
);
}
private static final String OAUTH_SCHEME_NAME = "my_oAuth_security_schema";
@Bean
public SecurityConfiguration security() {
return SecurityConfigurationBuilder.builder()
.clientId(clientId)
.clientSecret(clientSecret)
.realm(realm)
.appName(clientId)
.scopeSeparator(",")
.additionalQueryStringParams(null)
.useBasicAuthenticationWithAccessCodeGrant(false)
.build();
public OpenAPI openAPI() {
return new OpenAPI().components(new Components()
.addSecuritySchemes(OAUTH_SCHEME_NAME, createOAuthScheme()))
.addSecurityItem(new SecurityRequirement().addList(OAUTH_SCHEME_NAME))
.info(new Info().title("Todos Management Service")
.description("A service providing todos.")
.version("1.0"));
}
private SecurityScheme createOAuthScheme() {
OAuthFlows flows = createOAuthFlows();
return new SecurityScheme().type(SecurityScheme.Type.OAUTH2)
.flows(flows);
}
private OAuthFlows createOAuthFlows() {
OAuthFlow flow = createAuthorizationCodeFlow();
return new OAuthFlows().implicit(flow);
}
private OAuthFlow createAuthorizationCodeFlow() {
return new OAuthFlow()
.authorizationUrl(authServerUrl + "/realms/" + realm + "/protocol/openid-connect/auth")
.scopes(new Scopes().addString("read_access", "read data")
.addString("write_access", "modify data"));
}
}

View File

@ -1,36 +0,0 @@
package com.baeldung.swaggerkeycloak;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import static springfox.documentation.builders.RequestHandlerSelectors.basePackage;
@EnableOpenApi
@Configuration
class SwaggerUIConfig {
@Bean
Docket api() {
return new Docket(DocumentationType.OAS_30)
.useDefaultResponseMessages(false)
.select()
.apis(basePackage(TodosApplication.class.getPackage().getName()))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder().title("Todos Management Service")
.description("A service providing todos.")
.version("1.0")
.build();
}
}

View File

@ -1,14 +1,15 @@
package com.baeldung.swaggerkeycloak;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import javax.annotation.PostConstruct;
import java.time.LocalDate;
import java.util.Collection;
@ -28,13 +29,10 @@ public class TodosController {
}
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation("Read all todos")
@ApiResponses({
@ApiResponse(code = 200, message = "The todos were found and returned.")
})
@Operation(description = "Read all todos")
@ApiResponses({ @ApiResponse(responseCode = "200", description = "The todos were found and returned.") })
@PreAuthorize("hasAuthority('SCOPE_read_access')")
public Collection<Todo> readAll() {
return todos.values();
}
}

View File

@ -1,75 +0,0 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
window.addEventListener('DOMContentLoaded', function () {
run();
});
</script>
</body>
</html>

View File

@ -1,8 +1,19 @@
server:
port: 8081
keycloak:
auth-server-url: https://api.example.com/auth # Keycloak server url
realm: todos-service-realm # Keycloak Realm
resource: todos-service-clients # Keycloak Client
principal-attribute: preferred_username
ssl-required: external
credentials:
secret: 00000000-0000-0000-0000-000000000000
auth-server-url: http://localhost:8080 # Keycloak server url
realm: SpringBootKeycloak # Keycloak Realm
spring:
security:
oauth2:
resourceserver:
jwt.issuer-uri: http://localhost:8080/realms/SpringBootKeycloak
springdoc:
swagger-ui:
oauth:
client-id: login-app