Prerequisite
- Java 17
- Maven 3.6.3 or later
- Spring 3.1.2
- Spring Security 6.1.2
- Keycloak. I will use the commercial version RH SSO 7.6.0
- OAuth2 Authorization Code Grand https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
RH SSO/Keycloak
Download, unzip and create initial admin user and finally start at http://127.0.0.1:8180/. You could also use a Docker container.
$ ./add-user-keycloak.sh -u admin
$ ./standalone.sh -Djboss.socket.binding.port-offset=100
In Keycloak create
- New Realm demo
- Role USER
- User john with password
- Assign role USER to user john
- Create OIDC client with Client ID spring-boot3-oauth2-login and Root URL http://localhost:8080/
Application
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>3.1.2</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>se.mkk</groupId>
<artifactId>spring-boot-3-oauth2-login-keycloak</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-3-oauth2-login-keycloak</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</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-security</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-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
</project>
src/main/resources/application.properties
# OAuth2 Log In Spring Boot 2.x Property Mappings
# https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html#oauth2login-boot-property-mappings
spring.security.oauth2.client.registration.keycloak.client-id = spring-boot3-oauth2-login
spring.security.oauth2.client.registration.keycloak.client-secret = CHANGEME!!!
#spring.security.oauth2.client.registration.keycloak.client-authentication-method =
spring.security.oauth2.client.registration.keycloak.authorization-grant-type = authorization_code
#spring.security.oauth2.client.registration.keycloak.redirect-uri =
spring.security.oauth2.client.registration.keycloak.scope = openid
#spring.security.oauth2.client.registration.keycloak.client-name =
#spring.security.oauth2.client.provider.keycloak.authorization-uri
#spring.security.oauth2.client.provider.keycloak.token-uri
#spring.security.oauth2.client.provider.keycloak.jwk-set-uri
spring.security.oauth2.client.provider.keycloak.issuer-uri = http://localhost:8180/auth/realms/demo
#spring.security.oauth2.client.provider.keycloak.user-info-uri
#spring.security.oauth2.client.provider.keycloak.user-info-authentication-method
spring.security.oauth2.client.provider.keycloak.user-name-attribute = preferred_username
Simple REST API
package se.mkk.springboot3oauth2loginkeycloak;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
package se.mkk.springboot3oauth2loginkeycloak;
import java.security.Principal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping
public Map<String, String> getUser(HttpServletRequest request, Principal principal) {
Map<String, String> rtn = new LinkedHashMap<>();
rtn.put("request.getRemoteUser()", request.getRemoteUser());
rtn.put("request.isUserInRole(\"USER\")", Boolean.toString(request.isUserInRole("USER")));
rtn.put("request.getUserPrincipal().getClass()", request.getUserPrincipal().getClass().getName());
rtn.put("principal.getClass().getName()", principal.getClass().getName());
rtn.put("principal.getName()", principal.getName());
if (principal instanceof OAuth2AuthenticationToken token) {
List<String> authorities = token.getAuthorities().stream()
.map(grantedAuthority -> grantedAuthority.getAuthority()).toList();
rtn.put("OAuth2AuthenticationToken.getAuthorities()", authorities.toString());
}
return rtn;
}
}
OAuth2 Log in
ppackage se.mkk.springboot3oauth2loginkeycloak;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.web.SecurityFilterChain;
import com.nimbusds.jose.util.JSONObjectUtils;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class OAuth2LoginSecurityConfig {
// https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html#oauth2login-provide-securityfilterchain-bean
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http //
.authorizeHttpRequests(authorize -> authorize //
.anyRequest().authenticated()) //
.oauth2Login(oauth2 -> oauth2 //
.userInfoEndpoint(userInfo -> userInfo //
.oidcUserService(this.oidcUserService())));
return http.build();
}
// https://docs.spring.io/spring-security/reference/servlet/oauth2/login/advanced.html#oauth2login-advanced-map-authorities-oauth2userservice
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
OidcUser oidcUser = delegate.loadUser(userRequest);
OAuth2AccessToken accessToken = userRequest.getAccessToken();
Collection<GrantedAuthority> mappedAuthorities = new HashSet<>();
// 1) Fetch the authority information from the protected resource using accessToken
// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
try {
String[] chunks = accessToken.getTokenValue().split("\\.");
Base64.Decoder decoder = Base64.getUrlDecoder();
String header = new String(decoder.decode(chunks[0]));
String payload = new String(decoder.decode(chunks[1]));
Map<String, Object> claims = JSONObjectUtils.parse(payload);
mappedAuthorities = new KeycloakAuthoritiesConverter().convert(claims);
} catch (Exception e) {
e.printStackTrace();
}
// 3) Create a copy of oidcUser but use the mappedAuthorities instead
oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo(),
"preferred_username");
return oidcUser;
};
}
// Spring OAuth2 uses default Scopes Not Roles for Authorization
// org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
private class KeycloakAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
return convert(jwt.getClaims());
}
public Collection<GrantedAuthority> convert(Map<String, Object> claims) {
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (String authority : getAuthorities(claims)) {
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + authority));
}
return grantedAuthorities;
}
private Collection<String> getAuthorities(Map<String, Object> claims) {
Object realm_access = claims.get("realm_access");
if (realm_access instanceof Map) {
Map<String, Object> map = castAuthoritiesToMap(realm_access);
Object roles = map.get("roles");
if (roles instanceof Collection) {
return castAuthoritiesToCollection(roles);
}
}
return Collections.emptyList();
}
@SuppressWarnings("unchecked")
private Map<String, Object> castAuthoritiesToMap(Object authorities) {
return (Map<String, Object>) authorities;
}
@SuppressWarnings("unchecked")
private Collection<String> castAuthoritiesToCollection(Object authorities) {
return (Collection<String>) authorities;
}
}
}
And now run
$ mvn clean install spring-boot:run
And login and call REST endpoint
request.getRemoteUser() "john"
request.isUserInRole("USER") "true"
request.getUserPrincipal().getClass() "org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken"
principal.getClass().getName() "org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken"
principal.getName() "john"
OAuth2AuthenticationToken.getAuthorities() "[ROLE_USER, ROLE_default-roles-demo, ROLE_offline_access, ROLE_uma_authorization]"
Summary
You also want to configure Spring Session, Logout and CSRF.
Source code https://github.com/magnuskkarlsson/spring-boot-3-oauth2-login-keycloak
No comments:
Post a Comment