Skip to content

Commit

Permalink
Require salt and iterationCount configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
KochTobi committed Apr 8, 2024
1 parent 3ef8764 commit 61dd09e
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 129 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import javax.sql.DataSource;
import life.qbic.data_download.rest.security.QBiCTokenAuthenticationFilter;
import life.qbic.data_download.rest.security.QBiCTokenAuthenticationProvider;
import life.qbic.data_download.rest.security.QBiCTokenMatcher;
import life.qbic.data_download.rest.security.QBicTokenEncoder;
import life.qbic.data_download.rest.security.RequestAuthorizationManagerFactory;
import life.qbic.data_download.rest.security.TokenMatcher;
import life.qbic.data_download.rest.security.TokenEncoder;
import life.qbic.data_download.rest.security.acl.MeasurementMappingService;
import life.qbic.data_download.rest.security.acl.QBiCMeasurementMappingService;
import life.qbic.data_download.rest.security.acl.QbicPermissionEvaluator;
Expand Down Expand Up @@ -64,16 +64,18 @@ public class SecurityConfig {


@Bean("accessTokenEncoder")
public TokenMatcher tokenEncoder() {
return new QBiCTokenMatcher();
public TokenEncoder tokenEncoder(
@Value("${qbic.access-token.salt}") String salt,
@Value("${qbic.access-token.iteration-count}") int iterationCount) {
return new QBicTokenEncoder(salt, iterationCount);
}

@Bean("tokenAuthenticationProvider")
public QBiCTokenAuthenticationProvider authenticationProvider(
@Qualifier("accessTokenEncoder") TokenMatcher tokenMatcher,
@Qualifier("accessTokenEncoder") TokenEncoder tokenEncoder,
EncodedAccessTokenRepository encodedAccessTokenRepository,
UserDetailsRepository userDetailsRepository) {
return new QBiCTokenAuthenticationProvider(tokenMatcher, encodedAccessTokenRepository,
return new QBiCTokenAuthenticationProvider(tokenEncoder, encodedAccessTokenRepository,
userDetailsRepository);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
*/
public class QBiCTokenAuthenticationProvider implements AuthenticationProvider {

private final TokenMatcher tokenMatcher;
private final TokenEncoder tokenEncoder;
private final EncodedAccessTokenRepository encodedAccessTokenRepository;
private final UserDetailsRepository userDetailsRepository;

public QBiCTokenAuthenticationProvider(TokenMatcher tokenMatcher,
public QBiCTokenAuthenticationProvider(TokenEncoder tokenEncoder,
EncodedAccessTokenRepository encodedAccessTokenRepository,
UserDetailsRepository userDetailsRepository) {
this.tokenMatcher = requireNonNull(tokenMatcher, "tokenMatcher must not be null");
this.tokenEncoder = requireNonNull(tokenEncoder, "tokenEncoder must not be null");
this.encodedAccessTokenRepository = requireNonNull(encodedAccessTokenRepository,
"encodedAccessTokenRepository must not be null");
this.userDetailsRepository = requireNonNull(userDetailsRepository,
Expand All @@ -37,13 +37,10 @@ public QBiCTokenAuthenticationProvider(TokenMatcher tokenMatcher,
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication instanceof QBiCTokenAuthenticationRequest authenticationRequest) {
String token = authenticationRequest.getToken();
EncodedAccessToken encodedAccessToken = encodedAccessTokenRepository.findAll()
.parallelStream()
.filter(storedToken -> tokenMatcher.matches(token.toCharArray(),
storedToken.getAccessToken()))
.findAny().orElseThrow(
() -> new BadCredentialsException("not a valid token")
);
String encodedToken = tokenEncoder.encode(token);
EncodedAccessToken encodedAccessToken = encodedAccessTokenRepository
.findByAccessTokenEquals(encodedToken)
.orElseThrow(() -> new BadCredentialsException("not a valid token"));
if (encodedAccessToken.isExpired()) {
throw new CredentialsExpiredException("expired token");
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package life.qbic.data_download.rest.security;

import static java.util.Objects.requireNonNull;

import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.HexFormat;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

/**
* A token encoder
*/
public class QBicTokenEncoder implements TokenEncoder {

private static final int EXPECTED_MIN_SALT_BITS = 128;
private static final int EXPECTED_MIN_SALT_BYTES = (int) Math.ceil(
(double) EXPECTED_MIN_SALT_BITS / 8);
private static final int EXPECTED_MIN_ITERATION_COUNT = 100_000;

private final byte[] salt;
private final int iterationCount;

public QBicTokenEncoder(String salt, int iterationCount) {
this.salt = fromHex(requireNonNull(salt, "salt must not be null"));
if (this.salt.length < EXPECTED_MIN_SALT_BYTES) {
throw new IllegalArgumentException(
"salt must have at least " + EXPECTED_MIN_SALT_BITS + " bits.");
}
if (iterationCount < EXPECTED_MIN_ITERATION_COUNT) {
throw new IllegalArgumentException(
"Iteration count n=" + iterationCount + " cannot be less than n="
+ EXPECTED_MIN_ITERATION_COUNT);
}
this.iterationCount = iterationCount;
}

@Override
public String encode(String token) {
byte[] hash = pbe(token.toCharArray(), salt, iterationCount);
return iterationCount + ":" + toHex(salt) + ":" + toHex(hash);
}

private record EncryptionSettings(String cipher, int keyBitSize) {
}

private static final EncryptionSettings ENCRYPTION_SETTINGS = new EncryptionSettings(
"AES",
256
);

private static byte[] fromHex(String hex) {
return HexFormat.of().parseHex(hex);
}

private static String toHex(byte[] bytes) {
HexFormat hexFormat = HexFormat.of();
return hexFormat.formatHex(bytes);
}

/**
* Uses Password-Based Encryption to encrypt a token given a salt and iterations
* @param token the token to be encrypted
* @param salt the salt used in the encryption
* @param iterationCount the number of iterations
* @return encryption result
*/
private static byte[] pbe(char[] token, byte[] salt, int iterationCount) {
KeySpec spec =
new PBEKeySpec(token, salt, iterationCount, ENCRYPTION_SETTINGS.keyBitSize());
SecretKey secretKey;
try {
SecretKeyFactory result;
result = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
SecretKeyFactory factory = result;
secretKey = factory.generateSecret(spec);
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
throw new RuntimeException("error encrypting token: " + e.getMessage());
}
return new SecretKeySpec(secretKey.getEncoded(), ENCRYPTION_SETTINGS.cipher()).getEncoded();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package life.qbic.data_download.rest.security;

/**
* Encodes QBiC Access Tokens
*/
public interface TokenEncoder {

String encode(String token);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package life.qbic.data_download.rest.security.jpa.token;

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import org.springframework.data.repository.Repository;


public interface EncodedAccessTokenRepository extends JpaRepository<EncodedAccessToken, Integer> {
public interface EncodedAccessTokenRepository extends Repository<EncodedAccessToken, Integer> {

Optional<EncodedAccessToken> findByAccessTokenEquals(String accessToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"name": "openbis.filename.ignored-prefix",
"type": "java.lang.String",
"description": "A prefix that is ignored in the filepaths. Defaults to original/"
"description": "A prefix that is stripped from the file's path."
},
{
"name": "qbic.access-management.datasource.url",
Expand All @@ -19,5 +19,15 @@
"name": "qbic.access-management.datasource.password",
"type": "java.lang.String",
"description": "The password for the datasource from which access control information is fetched."
},
{
"name": "qbic.access-token.salt",
"type": "java.lang.String",
"description": "A hex string used as salt for the personal access tokens."
},
{
"name": "qbic.access-token.iteration-count",
"type": "java.lang.String",
"description": "The number of iterations used for encrypting personal access tokens."
}
] }
2 changes: 2 additions & 0 deletions rest-api/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ qbic.access-management.datasource.url=${ACCESS_DB_URL:${spring.datasource.url}}
qbic.access-management.datasource.username=${ACCESS_DB_USER_NAME:${spring.datasource.username}}
qbic.access-management.datasource.password=${ACCESS_DB_USER_PASSWORD:${spring.datasource.password}}
qbic.access-management.datasource.driver-class-name=${ACCESS_DB_DRIVER:${spring.datasource.driver-class-name}}
qbic.access-token.salt=${ACCESS_TOKEN_SALT:}
qbic.access-token.iteration-count=${ACCESS_TOKEN_ITERATIONS:100000}

### server settings
server.download.token-name=${TOKEN_NAME:Bearer}
Expand Down

This file was deleted.

Loading

0 comments on commit 61dd09e

Please sign in to comment.