Skip to content

Commit

Permalink
adds jwt auth (#269)
Browse files Browse the repository at this point in the history
* initial jwt auth setup

* refactor JWTConverter

* adds test for jwt auth

* use existing object id

* remove commented code

* make requested formating changes and add retry template

* add more and clean up JWTSecurityTests

* make requested changes, update variables and fix typo
  • Loading branch information
jaserud authored Aug 24, 2020
1 parent 1268b8f commit 12863cd
Show file tree
Hide file tree
Showing 17 changed files with 937 additions and 55 deletions.
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

0 comments on commit 12863cd

Please sign in to comment.