Commit dc98f654 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Removed Spring Security: calls handled using IA2 AuthLib

parent 4e96f2f0
......@@ -24,15 +24,6 @@
<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.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
......@@ -42,6 +33,16 @@
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>AuthLib</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
......
package it.inaf.ia2.gms.authn;
import java.util.Collection;
import java.util.Map;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
public class CustomAuthenticationData extends UsernamePasswordAuthenticationToken {
private final Map<String, Object> attributes;
private final OAuth2AccessToken accessToken;
private final String refreshToken;
public CustomAuthenticationData(String username, Map<String, Object> attributes,
Collection<? extends GrantedAuthority> authorities,
OAuth2AccessToken accessToken, String refreshToken) {
super(username, "N/A", authorities);
this.attributes = attributes;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
public Map<String, Object> getAttributes() {
return attributes;
}
public OAuth2AccessToken getAccessToken() {
return accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
}
package it.inaf.ia2.gms.authn;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import java.util.List;
import java.util.Map;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore;
public class CustomIdTokenConverter extends DefaultUserAuthenticationConverter {
private final JwkTokenStore jwkTokenStore;
private final LoggingDAO loggingDAO;
public CustomIdTokenConverter(JwkTokenStore jwkTokenStore, LoggingDAO loggingDAO) {
this.jwkTokenStore = jwkTokenStore;
this.loggingDAO = loggingDAO;
}
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
String idTokenString = (String) map.get("id_token");
String accessTokenString = (String) map.get("access_token");
// Needed for Franco's version: access_token is equal to id_token
if (accessTokenString == null) {
accessTokenString = idTokenString;
}
OAuth2AccessToken token = jwkTokenStore.readAccessToken(idTokenString);
OAuth2AccessToken accessToken = jwkTokenStore.readAccessToken(accessTokenString);
String refreshToken = (String) map.get("refresh_token");
Map<String, Object> claims = token.getAdditionalInformation();
String principal = (String) claims.get("sub");
loggingDAO.logAction("Login by " + principal);
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
return new CustomAuthenticationData(principal, claims, authorities, accessToken, refreshToken);
}
}
package it.inaf.ia2.gms.authn;
import java.util.Map;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
/**
* Retrieve token data from the user info / check token endpoint using the
* returned access token.
*/
public class GetTokenDataService implements ResourceServerTokenServices {
private final RestOperations restTemplate = new RestTemplate();
private String checkTokenEndpointUrl;
private String clientId;
private AccessTokenConverter tokenConverter;
public void setClientId(String clientId) {
this.clientId = clientId;
}
public void setCheckTokenEndpointUrl(String checkTokenEndpointUrl) {
this.checkTokenEndpointUrl = checkTokenEndpointUrl;
}
public void setAccessTokenConverter(AccessTokenConverter accessTokenConverter) {
this.tokenConverter = accessTokenConverter;
}
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("client_id", clientId);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + accessToken);
Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);
return this.tokenConverter.extractAuthentication(map);
}
private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
if (headers.getContentType() == null) {
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
}
return restTemplate.exchange(path, HttpMethod.POST,
new HttpEntity<>(formData, headers), Map.class).getBody();
}
@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
}
package it.inaf.ia2.gms.authn;
import it.inaf.ia2.aa.LoginFilter;
import java.io.IOException;
import java.util.Arrays;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.util.AntPathMatcher;
public class GmsLoginFilter extends LoginFilter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain fc) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if (shouldNotFilter(request)) {
fc.doFilter(req, res);
} else {
super.doFilter(req, res, fc);
}
}
private boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
// Allow CORS check
if ("OPTIONS".equals(request.getMethod())) {
return true;
}
AntPathMatcher pathMatcher = new AntPathMatcher();
// Authentication is ignored for these endpoints:
return Arrays.asList("/ws/jwt/**", "/error", "/logout", "/invited-registration", "/help/**")
.stream()
.anyMatch(p -> pathMatcher.match(p, request.getServletPath()));
}
}
package it.inaf.ia2.gms.authn;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SigningKeyResolver;
import it.inaf.ia2.aa.ServiceLocator;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import java.io.IOException;
import java.security.Principal;
......@@ -12,17 +16,15 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore;
public class JWTFilter implements Filter {
private final JwkTokenStore jwkTokenStore;
private final LoggingDAO loggingDAO;
private final SigningKeyResolver signingKeyResolver;
public JWTFilter(JwkTokenStore jwkTokenStore, LoggingDAO loggingDAO) {
this.jwkTokenStore = jwkTokenStore;
public JWTFilter(LoggingDAO loggingDAO) {
this.loggingDAO = loggingDAO;
this.signingKeyResolver = ServiceLocator.getInstance().getTokenManager().getSigningKeyResolver();
}
@Override
......@@ -40,14 +42,11 @@ public class JWTFilter implements Filter {
authHeader = authHeader.replace("Bearer", "").trim();
OAuth2AccessToken accessToken = jwkTokenStore.readAccessToken(authHeader);
if (accessToken.isExpired()) {
loggingDAO.logAction("Attempt to access WS with expired token", request);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access token is expired");
return;
}
Jwt jwt = Jwts.parser()
.setSigningKeyResolver(signingKeyResolver)
.parse(authHeader);
Map<String, Object> claims = accessToken.getAdditionalInformation();
Map<String, Object> claims = (Map<String, Object>) jwt.getBody();
if (claims.get("sub") == null) {
loggingDAO.logAction("Attempt to access WS with invalid token", request);
......
package it.inaf.ia2.gms.authn;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import java.util.List;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.resource.DefaultUserInfoRestTemplateFactory;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateCustomizer;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore;
import org.springframework.web.client.RestTemplate;
/**
* Extending the AuthorizationServerEndpointsConfiguration disables the Spring
* Boot ResourceServerTokenServicesConfiguration.
*/
@Configuration
public class OAuth2Config extends AuthorizationServerEndpointsConfiguration {
@Value("${security.oauth2.resource.token-info-uri}")
private String checkTokenEndpointUrl;
@Value("${security.oauth2.client.client-id}")
private String clientId;
@Bean
public ResourceServerTokenServices resourceServerTokenServices(JwkTokenStore jwkTokenStore, LoggingDAO loggingDAO) {
GetTokenDataService tokenService = new GetTokenDataService();
DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
accessTokenConverter.setUserTokenConverter(new CustomIdTokenConverter(jwkTokenStore, loggingDAO));
tokenService.setAccessTokenConverter(accessTokenConverter);
tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
tokenService.setClientId(clientId);
return tokenService;
}
@Bean
public ClientDetailsService clientDetailsService() {
return new InMemoryClientDetailsService();
}
@Bean
public UserInfoRestTemplateFactory userInfoRestTemplateFactory(
ObjectProvider<List<UserInfoRestTemplateCustomizer>> customizers,
ObjectProvider<OAuth2ProtectedResourceDetails> details,
ObjectProvider<OAuth2ClientContext> oauth2ClientContext) {
return new DefaultUserInfoRestTemplateFactory(customizers, details,
oauth2ClientContext);
}
@Bean
public RestTemplate rapRestTemplate() {
return new RestTemplate();
}
}
package it.inaf.ia2.gms.authn;
import it.inaf.ia2.gms.persistence.LoggingDAO;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.store.jwk.JwkTokenStore;
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableOAuth2Sso
public class SecurityConfig extends WebSecurityConfigurerAdapter {
public class SecurityConfig {
private static final Logger LOG = LoggerFactory.getLogger(SecurityConfig.class);
@Autowired
private Environment env;
@Value("${cors.allowed.origin}")
private String corsAllowedOrigin;
@Value("${security.oauth2.resource.jwk.key-set-uri}")
private String keySetUri;
/**
* CORS are necessary only for development (API access from npm server).
*/
@Bean
public JwkTokenStore jwkTokenStore() {
return new JwkTokenStore(keySetUri);
}
@Override
public void configure(HttpSecurity http) throws Exception {
@Profile("dev")
public WebMvcConfigurer corsConfigurer() {
// CORS are necessary only for development (API access from npm server)
if (Arrays.asList(env.getActiveProfiles()).contains("dev")) {
http.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
}
return new WebMvcConfigurer() {
super.configure(http);
@Override
public void addCorsMappings(CorsRegistry registry) {
// avoid displaying the annoying BasicAuth browser popup when the
// session expires (this should happen mostly during development)
// [401 WWW-Authenticate is converted to 403]
http.exceptionHandling().defaultAuthenticationEntryPointFor(
new Http403ForbiddenEntryPoint(), new AntPathRequestMatcher("/keepAlive"));
LOG.warn("Development profile active: CORS filter enabled");
http.csrf().disable();
}
/**
* The authentication is ignored for these endpoints. The "/ws/basic"
* endpoints (web service API for programmatic access) are protected by the
* custom ServiceBasicAuthFilter that checks BasicAuth for GMS clients,
* while the "/ws/jwt" endpoints are protected by the JWTFilter.
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/ws/jwt/**", "/error", "/logout", "/invited-registration", "/help/**");
registry.addMapping("/**")
.allowedOrigins(corsAllowedOrigin)
.allowedMethods("*")
.allowCredentials(true);
}
};
}
@Bean
......@@ -88,32 +56,24 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
* Checks JWT for web services.
*/
@Bean
public FilterRegistrationBean serviceJWTFilter(JwkTokenStore jwkTokenStore, LoggingDAO loggingDAO) {
public FilterRegistrationBean serviceJWTFilter(LoggingDAO loggingDAO) {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new JWTFilter(jwkTokenStore, loggingDAO));
bean.setFilter(new JWTFilter(loggingDAO));
bean.addUrlPatterns("/ws/jwt/*");
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
/**
* CORS are necessary only for development (API access from npm server).
*/
@Bean
@Profile("dev")
public FilterRegistrationBean corsFilter() {
LOG.warn("Development profile active: CORS filter enabled");
public FilterRegistrationBean loginFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new GmsLoginFilter());
registration.addUrlPatterns("/*");
return registration;
}
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();
config.addAllowedMethod(HttpMethod.PUT);
config.addAllowedMethod(HttpMethod.DELETE);
config.setAllowedOrigins(Arrays.asList(corsAllowedOrigin));
config.setAllowCredentials(true);
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
@Bean
public RestTemplate rapRestTemplate() {
return new RestTemplate();
}
}
package it.inaf.ia2.gms.authn;
import it.inaf.ia2.aa.data.User;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;
......@@ -11,6 +12,8 @@ import org.springframework.web.context.annotation.SessionScope;
@SessionScope
public class SessionData {
private static final String USER_DATA = "user_data";
@Autowired
private HttpServletRequest request;
......@@ -22,12 +25,16 @@ public class SessionData {
@PostConstruct
public void init() {
CustomAuthenticationData authn = (CustomAuthenticationData) ((OAuth2Authentication) request.getUserPrincipal()).getUserAuthentication();
userId = (String) authn.getPrincipal();
userName = (String) authn.getAttributes().get("name");
accessToken = (String) authn.getAccessToken().getValue();
refreshToken = authn.getRefreshToken();
setExpiresIn(authn.getAccessToken().getExpiresIn());
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute(USER_DATA) != null) {
User user = (User) session.getAttribute(USER_DATA);
userId = user.getName();
userName = user.getUserLabel();
accessToken = user.getAccessToken();
refreshToken = user.getRefreshToken();
setExpiresIn(user.getExpiresIn());
}
}
public String getUserId() {
......@@ -54,7 +61,7 @@ public class SessionData {
return userName;
}
public void setExpiresIn(int expiresIn) {
public void setExpiresIn(long expiresIn) {
this.expiration = System.currentTimeMillis() + expiresIn * 1000;
}
......
......@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import it.inaf.ia2.gms.authn.SessionData;
import it.inaf.ia2.gms.model.RapUser;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
......@@ -12,7 +13,6 @@ import java.util.Set;
import java.util.function.Function;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -152,7 +152,7 @@ public class RapClient {
if (basicAuth) { // Franco's version
String auth = clientId + ":" + clientSecret;
String encodedAuth = Base64.encodeBase64String(auth.getBytes());
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
headers.add("Authorization", "Basic " + encodedAuth);
HttpSession session = request.getSession(false);
......
client_id=gms
client_secret=gms-secret
access_token_uri=http://localhost/rap-ia2/auth/oauth2/token
user_authorization_uri=http://localhost/rap-ia2/auth/oauth2/authorize
check_token_uri=http://localhost/rap-ia2/auth/oauth2/token
jwks_uri=http://localhost/rap-ia2/auth/oidc/jwks
gms_uri=http://localhost:8082/gms/ws/jwt
groups_autoload=false
store_state_on_login_endpoint=true
scope=openid email profile read:rap
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment