Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds jwt auth #269

Merged
merged 8 commits into from
Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ services:
AUTH_SERVER_SCOPE_STUDY_PREFIX: score.
AUTH_SERVER_SCOPE_UPLOAD_SUFFIX: .WRITE
AUTH_SERVER_SCOPE_DOWNLOAD_SUFFIX: .READ
AUTH_SERVER_SCOPE_SYSTEM: score.WRITE
AUTH_SERVER_SCOPE_DOWNLOAD_SYSTEM: score.WRITE
AUTH_SERVER_SCOPE_UPLOAD_SYSTEM: score.READ
SERVER_SSL_ENABLED: "false"
UPLOAD_PARTSIZE: 1073741824
UPLOAD_CONNECTION_TIMEOUT: 1200000
Expand Down
8 changes: 7 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF S
<artifactId>spring-retry</artifactId>
<version>${spring-retry.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${spring-security-jwt.version}</version>
</dependency>

<!-- Spring Cloud -->
<dependency>
Expand Down Expand Up @@ -227,7 +232,8 @@ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF S
<!-- Versions - Spring -->
<spring-boot.version>2.1.6.RELEASE</spring-boot.version>
<spring-retry.version>1.1.2.RELEASE</spring-retry.version>
<spring-security-oauth2.version>2.0.7.RELEASE</spring-security-oauth2.version>
<spring-security-oauth2.version>2.3.5.RELEASE</spring-security-oauth2.version>
<spring-security-jwt.version>1.1.1.RELEASE</spring-security-jwt.version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>

<!-- Versions - Amazon -->
Expand Down
27 changes: 26 additions & 1 deletion score-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,21 @@ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF S
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>

<!-- Amazon -->
<dependency>
<groupId>com.amazonaws</groupId>
Expand Down Expand Up @@ -124,7 +133,23 @@ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF S
<version>1.2.2</version><!--$NO-MVN-MAN-VER$-->
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>5.1.9.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<properties>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2018. Ontario Institute for Cancer Research
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package bio.overture.score.server.config;

import com.google.common.collect.ImmutableMap;
import lombok.val;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.ResourceAccessException;

import java.util.Map;

import static java.lang.Boolean.TRUE;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.springframework.retry.backoff.ExponentialBackOffPolicy.DEFAULT_MULTIPLIER;

@Configuration
public class RetryConfig {

private static final int DEFAULT_MAX_RETRIES = 5;
private static final long DEFAULT_INITIAL_BACKOFF_INTERVAL = SECONDS.toMillis(15L);

@Value("${auth.connection.maxRetries}")
private int maxRetries = DEFAULT_MAX_RETRIES;

@Value("${auth.connection.initialBackoff}")
private long initialBackoff = DEFAULT_INITIAL_BACKOFF_INTERVAL;

@Value("${auth.connection.multiplier}")
private double multiplier = DEFAULT_MULTIPLIER;

@Bean
public RetryTemplate retryTemplate() {
val result = new RetryTemplate();
result.setBackOffPolicy(defineBackOffPolicy());

result.setRetryPolicy(
new SimpleRetryPolicy(maxRetries, getRetryableExceptions(), true));
return result;
}

private BackOffPolicy defineBackOffPolicy() {
val backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(initialBackoff);
backOffPolicy.setMultiplier(multiplier);

return backOffPolicy;
}

private static Map<Class<? extends Throwable>, Boolean> getRetryableExceptions() {
return ImmutableMap.of(
ResourceAccessException.class, TRUE,
HttpServerErrorException.class, TRUE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,19 @@

import bio.overture.score.server.metadata.MetadataService;
import bio.overture.score.server.properties.ScopeProperties;
import bio.overture.score.server.security.AccessTokenConverterWithExpiry;
import bio.overture.score.server.security.CachingRemoteTokenServices;
import bio.overture.score.server.security.scope.DownloadScopeAuthorizationStrategy;
import bio.overture.score.server.security.scope.UploadScopeAuthorizationStrategy;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor;
import org.springframework.security.oauth2.provider.authentication.TokenExtractor;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.web.filter.OncePerRequestFilter;

Expand Down Expand Up @@ -95,29 +86,6 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht
configureAuthorization(http);
}

@Bean
public AccessTokenConverter accessTokenConverter() {
return new AccessTokenConverterWithExpiry();
}

@Bean
public RemoteTokenServices remoteTokenServices(
final @Value("${auth.server.url}") String checkTokenUrl,
final @Value("${auth.server.tokenName:token}") String tokenName,
final @Value("${auth.server.clientId}") String clientId,
final @Value("${auth.server.clientSecret}") String clientSecret) {
val remoteTokenServices = new CachingRemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl(checkTokenUrl);
remoteTokenServices.setClientId(clientId);
remoteTokenServices.setClientSecret(clientSecret);
remoteTokenServices.setTokenName(tokenName);
remoteTokenServices.setAccessTokenConverter(accessTokenConverter());

log.debug("using auth server: " + checkTokenUrl);

return remoteTokenServices;
}

private void configureAuthorization(HttpSecurity http) throws Exception {
scopeProperties.logScopeProperties();;

Expand Down Expand Up @@ -153,4 +121,7 @@ public DownloadScopeAuthorizationStrategy accessSecurity(@Autowired MetadataServ
scopeProperties.getDownload().getSystem(),
song);
}
}

public ScopeProperties getScopeProperties() { return this.scopeProperties; }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package bio.overture.score.server.config;

import bio.overture.score.server.security.*;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.web.client.RestTemplate;

@Configuration
@Slf4j
@Profile("secure")
public class TokenServicesConfig {

@Value("${auth.server.url}") private String checkTokenUrl;
@Value("${auth.server.tokenName:token}") private String tokenName;
@Value("${auth.server.clientId}") private String clientId;
@Value("${auth.server.clientSecret}") private String clientSecret;

@Bean
@Profile("!jwt")
public RemoteTokenServices remoteTokenServices() {
return createRemoteTokenServices();
}

@Bean
@Autowired
@Profile("jwt")
public MergedServerTokenServices mergedServerTokenServices(
@NonNull PublicKeyFetcher publicKeyFetcher,
@NonNull RetryTemplate retryTemplate
) {
val jwtTokenServices = createJwtTokenServices(publicKeyFetcher.getPublicKey());
val remoteTokenServices = createRemoteTokenServices();
return new MergedServerTokenServices(jwtTokenServices, remoteTokenServices, retryTemplate);
}

@Bean
@Autowired
@Profile("jwt")
public PublicKeyFetcher publicKeyFetcher(
@Value("${auth.jwt.publicKeyUrl}") @NonNull String publicKeyUrl,
@NonNull RetryTemplate retryTemplate) {
return new DefaultPublicKeyFetcher(publicKeyUrl, new RestTemplate(), retryTemplate);
}

private AccessTokenConverter accessTokenConverter() {
return new AccessTokenConverterWithExpiry();
}

private RemoteTokenServices createRemoteTokenServices() {
val remoteTokenServices = new CachingRemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl(checkTokenUrl);
remoteTokenServices.setClientId(clientId);
remoteTokenServices.setClientSecret(clientSecret);
remoteTokenServices.setTokenName(tokenName);
remoteTokenServices.setAccessTokenConverter(accessTokenConverter());

log.debug("using auth server: " + checkTokenUrl);

return remoteTokenServices;
}

private DefaultTokenServices createJwtTokenServices(String publicKey) {
val tokenStore = new JwtTokenStore(new JWTConverter(publicKey));
val defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore);
return defaultTokenServices;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package bio.overture.score.server.security;

import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestTemplate;

@RequiredArgsConstructor
public class DefaultPublicKeyFetcher implements PublicKeyFetcher {

@NonNull private final String url;
@NonNull private final RestTemplate restTemplate;
@NonNull private final RetryTemplate retryTemplate;

@Override
public String getPublicKey() {
val resp = retryTemplate.execute(x -> restTemplate.getForEntity(url, String.class));
return resp.hasBody() ? resp.getBody() : null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package bio.overture.score.server.security;

import lombok.NonNull;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import java.util.*;

@Slf4j
public class JWTConverter extends JwtAccessTokenConverter {

private final static String CONTEXT = "context";
private final static String SCOPE = "scope";

@SneakyThrows
public JWTConverter(String publicKey) {
super();
this.setVerifierKey(publicKey);
this.afterPropertiesSet();
}

@Override
public OAuth2Authentication extractAuthentication(@NonNull Map<String, ?> map) {
// Currently EGO's JWT spec places scopes in map at 'context.scope'
// but extractAuthentication expects them in map's root at 'scope'
// so put all scopes into root level for spring security processing
val allScopes = getRootAndContextScopes(map);
HashMap<String, Object> updatedMap = new HashMap<>(map);
updatedMap.put(SCOPE, allScopes);

return super.extractAuthentication(updatedMap);
}

private Collection<String> getRootAndContextScopes(Map<String, ?> map) {
List<String> extractedScopes = new ArrayList<>(Collections.emptyList());
try {
if (map.containsKey(CONTEXT)) {
val context = (Map<String, Object>) map.get(CONTEXT);
extractedScopes.addAll((Collection<String>) context.get(SCOPE));
}
if (map.containsKey(SCOPE)) {
extractedScopes.addAll((Collection<String>) map.get(SCOPE));
}
} catch (Exception e) {
log.error("Failed to extract scopes from JWT");
}
return extractedScopes;
}
}
Loading