Last active
June 25, 2018 18:56
-
-
Save stuchl4n3k/d36736b2b127fa4976c2f764ef1a81f9 to your computer and use it in GitHub Desktop.
PoC for Spring Boot 2 + Spring Security 5 + Keycloak 3.4.3 without Keycloak Adapter
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.ArrayList; | |
import java.util.Collection; | |
import java.util.Collections; | |
import java.util.LinkedHashSet; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.Set; | |
import lombok.RequiredArgsConstructor; | |
import org.springframework.beans.factory.annotation.Value; | |
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; | |
import org.springframework.context.annotation.Bean; | |
import org.springframework.context.annotation.Configuration; | |
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | |
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; | |
import org.springframework.security.core.GrantedAuthority; | |
import org.springframework.security.core.authority.AuthorityUtils; | |
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; | |
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; | |
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.OAuth2AuthenticationException; | |
import org.springframework.security.oauth2.core.OAuth2Error; | |
import org.springframework.security.oauth2.core.OAuth2ErrorCodes; | |
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.oauth2.jwt.JwtDecoder; | |
import org.springframework.security.oauth2.jwt.JwtException; | |
import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport; | |
import org.springframework.util.CollectionUtils; | |
import static org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; | |
/** | |
* Security configuration with Keycloak auth. | |
* | |
* Based on a <a href="https://github.com/thomasdarimont/spring-boot-2-keycloak-oauth-example">PoC by Thomas Darimont</a> | |
* | |
* @author stuchl4n3k | |
*/ | |
@Configuration | |
public class KeycloakSecurityConfig { | |
private static final String OAUTH2_PROVIDER = "keycloak"; | |
/** | |
* Configures OAuth Login with Spring Security 5. | |
*/ | |
@Bean | |
public WebSecurityConfigurerAdapter webSecurityConfigurer(@Value("${keycloak.realm}") String realm, | |
KeycloakOauth2UserService keycloakOidcUserService) { | |
return new WebSecurityConfigurerAdapter() { | |
@Override | |
public void configure(HttpSecurity http) throws Exception { | |
http | |
// Allow everything here and restrict paths using method-level security. | |
.authorizeRequests().anyRequest().permitAll() | |
.and() | |
// This is the point where OAuth2 login of Spring 5 gets enabled. | |
.oauth2Login().userInfoEndpoint().oidcUserService(keycloakOidcUserService) | |
.and() | |
// I don't want a page with different clients as login options, | |
// so I use the constant from OAuth2AuthorizationRequestRedirectFilter | |
// plus the configured realm as immediate redirect to Keycloak. | |
.loginPage(DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + realm); | |
} | |
}; | |
} | |
@Bean | |
public KeycloakOauth2UserService keycloakOidcUserService(OAuth2ClientProperties oauth2ClientProperties) { | |
JwtDecoder jwtDecoder = new NimbusJwtDecoderJwkSupport( | |
oauth2ClientProperties.getProvider().get(OAUTH2_PROVIDER).getJwkSetUri() | |
); | |
SimpleAuthorityMapper authoritiesMapper = new SimpleAuthorityMapper(); | |
authoritiesMapper.setConvertToUpperCase(true); | |
return new KeycloakOauth2UserService(jwtDecoder, authoritiesMapper); | |
} | |
} | |
/** | |
* Keycloak-specific implementation of {@link OAuth2UserService} which augments | |
* the user authorities with Keycloak's client-mapped roles. | |
* | |
* @author stuchl4n3k | |
*/ | |
@RequiredArgsConstructor | |
class KeycloakOauth2UserService extends OidcUserService { | |
private static final OAuth2Error INVALID_REQUEST = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST); | |
private final JwtDecoder jwtDecoder; | |
private final GrantedAuthoritiesMapper authoritiesMapper; | |
/** | |
* Augments {@link OidcUserService#loadUser(OidcUserRequest)} to add authorities | |
* provided by Keycloak. | |
* | |
* Needed because {@link OidcUserService#loadUser(OidcUserRequest)} (currently) | |
* does not provide a hook for adding custom authorities from a{@link OidcUserRequest}. | |
*/ | |
@Override | |
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { | |
OidcUser user = super.loadUser(userRequest); | |
Collection<GrantedAuthority> keycloakAuthorities = extractKeycloakAuthorities(userRequest); | |
return withAuthorities(user, keycloakAuthorities); | |
} | |
/** | |
* Creates a new {@link OidcUser} with additional given {@code authorities}. | |
*/ | |
private OidcUser withAuthorities(OidcUser user, Collection<? extends GrantedAuthority> authorities) { | |
Set<GrantedAuthority> newAuthorities = new LinkedHashSet<>(); | |
newAuthorities.addAll(user.getAuthorities()); | |
newAuthorities.addAll(authorities); | |
return new DefaultOidcUser( | |
newAuthorities, | |
user.getIdToken(), | |
user.getUserInfo(), | |
"preferred_username" | |
); | |
} | |
/** | |
* Extracts {@link GrantedAuthority GrantedAuthorities} from the AccessToken in | |
* the {@link OidcUserRequest}. | |
*/ | |
private Collection<GrantedAuthority> extractKeycloakAuthorities(OidcUserRequest userRequest) { | |
Jwt token = parseJwt(userRequest.getAccessToken().getTokenValue()); | |
@SuppressWarnings("unchecked") | |
Map<String, Object> resourceMap = (Map<String, Object>) token.getClaims().get("resource_access"); | |
String clientId = userRequest.getClientRegistration().getClientId(); | |
@SuppressWarnings("unchecked") | |
Map<String, Map<String, Object>> clientResource = (Map<String, Map<String, Object>>) resourceMap.get(clientId); | |
if (CollectionUtils.isEmpty(clientResource)) { | |
return Collections.emptyList(); | |
} | |
@SuppressWarnings("unchecked") | |
List<String> clientRoles = (List<String>) clientResource.get("roles"); | |
if (CollectionUtils.isEmpty(clientRoles)) { | |
return Collections.emptyList(); | |
} | |
return map(AuthorityUtils.createAuthorityList(clientRoles.toArray(new String[0]))); | |
} | |
/** | |
* Maps given {@code authorities} using {@link #authoritiesMapper} if available. | |
*/ | |
private Collection<GrantedAuthority> map(Collection<GrantedAuthority> authorities) { | |
if (authoritiesMapper == null) { | |
return authorities; | |
} | |
return new ArrayList<>(authoritiesMapper.mapAuthorities(authorities)); | |
} | |
private Jwt parseJwt(String accessTokenValue) { | |
try { | |
// Token is already verified by spring security infrastructure. | |
return jwtDecoder.decode(accessTokenValue); | |
} catch (JwtException e) { | |
throw new OAuth2AuthenticationException(INVALID_REQUEST, e); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See https://github.com/thomasdarimont/spring-boot-2-keycloak-oauth-example for more info.