Skip to content

Instantly share code, notes, and snippets.

@stuchl4n3k
Last active June 25, 2018 18:56
Show Gist options
  • Save stuchl4n3k/d36736b2b127fa4976c2f764ef1a81f9 to your computer and use it in GitHub Desktop.
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
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);
}
}
}
@stuchl4n3k
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment