Commit eb97af3f authored by Matija Obreza's avatar Matija Obreza

Merge branch 'cors-origins-support' into 'master'

Implemented a CORS Origins filter

See merge request genesys-pgr/application-blocks!46
parents 55c83876 da0a58c8
......@@ -154,6 +154,16 @@ public class OAuthClient extends AclSid implements ClientDetails, Copyable<OAuth
@Type(type = "org.hibernate.type.TextType")
private String description;
/** Allowed CORS origins */
@Transient
@JsonView(JsonViews.Protected.class)
private Set<String> allowedOrigins = new HashSet<>();
/** The origins. */
@JsonIgnore
@Column(nullable = true, length = 200)
private String origins;
/**
* Instantiates a new o auth client.
*/
......@@ -179,6 +189,7 @@ public class OAuthClient extends AclSid implements ClientDetails, Copyable<OAuth
autoApproveScope = autoApproveScopes.stream().collect(Collectors.joining(";"));
grants = grantTypes.stream().collect(Collectors.joining(";"));
redirect = redirectUris.stream().collect(Collectors.joining(";"));
origins = allowedOrigins.stream().collect(Collectors.joining(";"));
}
/**
......@@ -201,6 +212,9 @@ public class OAuthClient extends AclSid implements ClientDetails, Copyable<OAuth
if (redirect != null) {
Arrays.stream(StringUtils.split(redirect, ";")).filter(r -> StringUtils.isNotBlank(r)).forEach(u -> redirectUris.add(u));
}
if (origins != null) {
Arrays.stream(StringUtils.split(origins, ";")).filter(r -> StringUtils.isNotBlank(r)).forEach(u -> allowedOrigins.add(u));
}
}
/*
......@@ -647,4 +661,20 @@ public class OAuthClient extends AclSid implements ClientDetails, Copyable<OAuth
return this;
}
public String getOrigins() {
return origins;
}
public void setOrigins(String origins) {
this.origins = origins;
}
public Set<String> getAllowedOrigins() {
return allowedOrigins;
}
public void setAllowedOrigins(Set<String> allowedOrigins) {
this.allowedOrigins = allowedOrigins;
}
}
/*
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.blocks.security.component;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.genesys.blocks.oauth.model.OAuthClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.filter.OncePerRequestFilter;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
/**
* Filter OAuth2 requests and check that Origin header matches what we have on
* file.
*/
public class OAuthClientOriginCheckFilter extends OncePerRequestFilter {
@Autowired
@Qualifier("oauthService")
private ClientDetailsService clientDetailsService;
private LoadingCache<String, Set<String>> clientOriginsCache = CacheBuilder.newBuilder().maximumSize(100).expireAfterWrite(10, TimeUnit.MINUTES).build(
new CacheLoader<String, Set<String>>() {
public Set<String> load(String clientId) {
if (logger.isInfoEnabled()) {
logger.info("Loading allowed origins for client: " + clientId);
}
OAuthClient clientDetails = (OAuthClient) clientDetailsService.loadClientByClientId(clientId);
return clientDetails.getAllowedOrigins();
}
});
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication instanceof OAuth2Authentication) {
if (!checkValidOrigin(request, (OAuth2Authentication) authentication)) {
response.sendError(403, "Request origin not valid");
return;
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Authentication null for origin: " + request.getHeader("Origin"));
}
}
filterChain.doFilter(request, response);
}
private boolean checkValidOrigin(HttpServletRequest request, OAuth2Authentication authAuth) {
String reqOrigin = request.getHeader("Origin");
if (authAuth.getOAuth2Request() != null) {
String clientId = authAuth.getOAuth2Request().getClientId();
try {
Set<String> allowedOrigins = clientOriginsCache.get(clientId);
if (!allowedOrigins.isEmpty()) {
if (reqOrigin == null) {
if (logger.isInfoEnabled()) {
logger.info("No origin header in request. Denying.");
}
return false;
}
for (String allowedOrigin : allowedOrigins) {
if (reqOrigin.startsWith(allowedOrigin)) {
if (logger.isDebugEnabled()) {
logger.debug("Origin match: " + reqOrigin + " startsWith " + allowedOrigin);
}
return true;
}
}
// No declared origins match
if (logger.isInfoEnabled()) {
logger.info("No origin match: " + reqOrigin + " in " + allowedOrigins.toString());
}
return false;
}
} catch (ExecutionException e) {
logger.warn("Error loading client origins", e);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Allowing request with Origin: " + reqOrigin);
}
return true;
}
}
\ No newline at end of file
Markdown is supported
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