This is an automated email from the ASF dual-hosted git repository.
martin_s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/archiva-redback-core.git
The following commit(s) were added to refs/heads/master by this push:
new 9ca5514 Adding implementation of JWT token authentication
9ca5514 is described below
commit 9ca5514bedce1b95c9b61df11aa18bdfb72b148c
Author: Martin Stockhammer <[email protected]>
AuthorDate: Thu Jul 9 22:27:33 2020 +0200
Adding implementation of JWT token authentication
---
.../redback/authentication/SimpleTokenData.java | 17 +-
.../{TokenData.java => StringToken.java} | 71 +--
.../authentication/{TokenData.java => Token.java} | 46 +-
.../archiva/redback/authentication/TokenData.java | 6 +-
.../redback-authentication-jwt/pom.xml | 12 +
.../authentication/jwt/JwtAuthenticator.java | 625 +++++++++++++++++++--
.../src/main/resources/META-INF/spring-context.xml | 34 ++
.../authentication/jwt/AbstractJwtTest.java | 241 ++++++++
.../JwtAuthenticatorFilebasedPublicKeyTest.java | 120 ++++
.../jwt/JwtAuthenticatorFilebasedTest.java | 107 ++++
.../jwt/JwtAuthenticatorMemorybasedTest.java | 75 +++
.../src/test/resources/log4j2-test.xml | 36 ++
.../src/test/resources/security.properties | 21 +
.../configuration/DefaultUserConfiguration.java | 2 +-
.../configuration/UserConfigurationKeys.java | 7 +
.../archiva/redback/config-defaults.properties | 3 +-
16 files changed, 1277 insertions(+), 146 deletions(-)
diff --git
a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java
b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java
index 16f6040..fc0de01 100644
---
a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java
+++
b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java
@@ -20,6 +20,8 @@ package org.apache.archiva.redback.authentication;
*/
import java.io.Serializable;
+import java.time.Duration;
+import java.time.Instant;
import java.util.Date;
/**
@@ -39,11 +41,10 @@ public final class SimpleTokenData implements Serializable,
TokenData {
private static final long serialVersionUID = 5907745449771921813L;
private final String user;
- private final Date created;
- private final Date validBefore;
+ private final Instant created;
+ private final Instant validBefore;
private final long nonce;
-
/**
* Creates a new token info instance for the given user.
* The lifetime in milliseconds defines the invalidation date by
@@ -55,8 +56,8 @@ public final class SimpleTokenData implements Serializable,
TokenData {
*/
public SimpleTokenData(final String user, final long lifetime, final long
nonce) {
this.user=user;
- this.created=new Date();
- this.validBefore =new Date(created.getTime()+lifetime);
+ this.created = Instant.now( );
+ this.validBefore = created.plus( Duration.ofMillis( lifetime ) );
this.nonce = nonce;
}
@@ -66,12 +67,12 @@ public final class SimpleTokenData implements Serializable,
TokenData {
}
@Override
- public final Date created() {
+ public final Instant created() {
return created;
}
@Override
- public final Date validBefore() {
+ public final Instant validBefore() {
return validBefore;
}
@@ -82,7 +83,7 @@ public final class SimpleTokenData implements Serializable,
TokenData {
@Override
public boolean isValid() {
- return (System.currentTimeMillis())<validBefore.getTime();
+ return Instant.now( ).isBefore( validBefore );
}
}
diff --git
a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java
b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/StringToken.java
similarity index 52%
copy from
redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java
copy to
redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/StringToken.java
index f641f3a..c96c4e2 100644
---
a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java
+++
b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/StringToken.java
@@ -10,7 +10,6 @@ package org.apache.archiva.redback.authentication;
* 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
@@ -19,48 +18,36 @@ package org.apache.archiva.redback.authentication;
* under the License.
*/
-import java.util.Date;
-
/**
+ * Simple token implementation. This implementation is immutable.
*
- * This contains the token payload that is used for verification of tokens.
- *
- * Created by Martin Stockhammer on 11.02.17.
+ * @author Martin Stockhammer <[email protected]>
*/
-public interface TokenData {
-
- /**
- * Returns the user name.
- *
- * @return The username property.
- */
- String getUser();
-
- /**
- * The date the token was created.
- *
- * @return The creation date.
- */
- Date created();
-
- /**
- * The date after that the token is invalid.
- *
- * @return The invalidation date.
- */
- Date validBefore();
-
- /**
- * The nonce that is stored in the token.
- *
- * @return The nonce.
- */
- long getNonce();
-
- /**
- * Returns true, if the token is valid.
- *
- * @return True, if valid, otherwise false.
- */
- boolean isValid();
+public class StringToken implements Token
+{
+ final TokenData metadata;
+ final String token;
+
+ public StringToken(String tokenData, TokenData metadata) {
+ this.token = tokenData;
+ this.metadata = metadata;
+ }
+
+ @Override
+ public String getData( )
+ {
+ return token;
+ }
+
+ @Override
+ public byte[] getBytes( )
+ {
+ return token.getBytes( );
+ }
+
+ @Override
+ public TokenData getMetadata( )
+ {
+ return metadata;
+ }
}
diff --git
a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java
b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/Token.java
similarity index 52%
copy from
redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java
copy to
redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/Token.java
index f641f3a..221a57b 100644
---
a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java
+++
b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/Token.java
@@ -10,7 +10,6 @@ package org.apache.archiva.redback.authentication;
* 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
@@ -19,48 +18,17 @@ package org.apache.archiva.redback.authentication;
* under the License.
*/
-import java.util.Date;
-
/**
+ * This interface represents a token including its metadata.
*
- * This contains the token payload that is used for verification of tokens.
- *
- * Created by Martin Stockhammer on 11.02.17.
+ * @author Martin Stockhammer <[email protected]>
*/
-public interface TokenData {
-
- /**
- * Returns the user name.
- *
- * @return The username property.
- */
- String getUser();
-
- /**
- * The date the token was created.
- *
- * @return The creation date.
- */
- Date created();
+public interface Token
+{
- /**
- * The date after that the token is invalid.
- *
- * @return The invalidation date.
- */
- Date validBefore();
+ String getData();
- /**
- * The nonce that is stored in the token.
- *
- * @return The nonce.
- */
- long getNonce();
+ byte[] getBytes();
- /**
- * Returns true, if the token is valid.
- *
- * @return True, if valid, otherwise false.
- */
- boolean isValid();
+ TokenData getMetadata();
}
diff --git
a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java
b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java
index f641f3a..d8f9c04 100644
---
a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java
+++
b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java
@@ -19,7 +19,7 @@ package org.apache.archiva.redback.authentication;
* under the License.
*/
-import java.util.Date;
+import java.time.Instant;
/**
*
@@ -41,14 +41,14 @@ public interface TokenData {
*
* @return The creation date.
*/
- Date created();
+ Instant created();
/**
* The date after that the token is invalid.
*
* @return The invalidation date.
*/
- Date validBefore();
+ Instant validBefore();
/**
* The nonce that is stored in the token.
diff --git
a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/pom.xml
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/pom.xml
index 77a6c65..b37670d 100644
---
a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/pom.xml
+++
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/pom.xml
@@ -77,6 +77,18 @@
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
+
+
+ <dependency>
+ <groupId>org.apache.archiva.components.registry</groupId>
+ <artifactId>archiva-components-spring-registry-commons</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-test</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
diff --git
a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticator.java
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticator.java
index 8ebe45f..f9a3a32 100644
---
a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticator.java
+++
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticator.java
@@ -19,18 +19,28 @@ package org.apache.archiva.redback.authentication.jwt;
* under the License.
*/
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.JwsHeader;
+import io.jsonwebtoken.Jwt;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.JwtParser;
+import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.SigningKeyResolverAdapter;
import io.jsonwebtoken.security.Keys;
import org.apache.archiva.redback.authentication.AbstractAuthenticator;
import org.apache.archiva.redback.authentication.AuthenticationDataSource;
import org.apache.archiva.redback.authentication.AuthenticationException;
import org.apache.archiva.redback.authentication.AuthenticationResult;
import org.apache.archiva.redback.authentication.Authenticator;
+import org.apache.archiva.redback.authentication.SimpleTokenData;
+import org.apache.archiva.redback.authentication.StringToken;
+import org.apache.archiva.redback.authentication.Token;
import
org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource;
+import org.apache.archiva.redback.authentication.TokenData;
import org.apache.archiva.redback.configuration.UserConfiguration;
-import org.apache.archiva.redback.configuration.UserConfigurationKeys;
-import org.apache.archiva.redback.policy.AccountLockedException;
-import org.apache.archiva.redback.policy.MustChangePasswordException;
+import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@@ -40,11 +50,13 @@ import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.inject.Named;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.Key;
import java.security.KeyFactory;
@@ -55,15 +67,39 @@ import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
+import java.time.Duration;
+import java.time.Instant;
import java.util.Base64;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.Map;
import java.util.Properties;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import static org.apache.archiva.redback.configuration.UserConfigurationKeys.*;
-@Service("authenticator#jwt")
+/**
+ * Authenticator for JWT tokens. This authenticator needs a secret key or
keypair depending
+ * on the used algorithm for signing and verification.
+ * The key can be either volatile in memory, which means a new one is created,
with each
+ * start of the service. Or it can be stored in a file.
+ * If this service is running in a cluster, you need a shared filesystem (NFS)
for storing
+ * the key file otherwise different keys will be used in each instance.
+ * <p>
+ * You can renew the used key ({@link #renewSigningKey()}). The authenticator
keeps a fixed
+ * sized list of the last keys used and stores the key identifier in the JWT
header.
+ * <p>
+ * The default algorithm for the JWT is currently {@link
org.apache.archiva.redback.configuration.UserConfigurationKeys#AUTHENTICATION_JWT_SIGALG_ES384}
+ */
+@Service( "authenticator#jwt" )
public class JwtAuthenticator extends AbstractAuthenticator implements
Authenticator
{
private static final Logger log = LoggerFactory.getLogger(
JwtAuthenticator.class );
+ public static final String DEFAULT_LIFETIME = "14400000";
+ public static final String DEFAULT_KEYFILE = "jwt-key.xml";
public static final String ID = "JwtAuthenticator";
public static final String PROP_PRIV_ALG = "privateAlgorithm";
public static final String PROP_PRIV_FORMAT = "privateFormat";
@@ -71,19 +107,52 @@ public class JwtAuthenticator extends
AbstractAuthenticator implements Authentic
public static final String PROP_PUB_FORMAT = "publicFormat";
public static final String PROP_PRIVATEKEY = "privateKey";
public static final String PROP_PUBLICKEY = "publicKey";
+ public static final String PROP_KEYID = "keyId";
+ private static final String ISSUER = "archiva.apache.org/redback";
@Inject
@Named( value = "userConfiguration#default" )
UserConfiguration userConfiguration;
- boolean symmetricAlg = true;
- Key key;
- Key publicKey;
- String sigAlg;
+ boolean symmetricAlgorithm = true;
+ boolean fileStore = false;
+ LinkedHashMap<Long, SecretKey> secretKey;
+ LinkedHashMap<Long, KeyPair> keyPair;
+ String signatureAlgorithm;
String keystoreType;
Path keystoreFilePath;
+ int maxInMemoryKeys = 5;
+ AtomicLong keyCounter;
+ final SigningKeyResolver resolver = new SigningKeyResolver( );
+ final ReadWriteLock lock = new ReentrantReadWriteLock( );
+ private JwtParser parser;
+ private Duration lifetime;
+ public class SigningKeyResolver extends SigningKeyResolverAdapter
+ {
+
+ @Override
+ public Key resolveSigningKey( JwsHeader jwsHeader, Claims claims )
+ {
+ Long keyId = Long.valueOf( jwsHeader.get( JwsHeader.KEY_ID
).toString() );
+ Key key;
+ if (symmetricAlgorithm) {
+ key = getSecretKey( keyId );
+ } else
+ {
+ KeyPair pair = getKeyPair( keyId );
+ if (pair == null) {
+ throw new JwtException( "Key ID not found in current list.
Verification failed." );
+ }
+ key = pair.getPublic( );
+ }
+ if (key==null) {
+ throw new JwtException( "Key ID not found in current list.
Verification failed." );
+ }
+ return key;
+ }
+ }
@Override
public String getId( )
@@ -92,62 +161,242 @@ public class JwtAuthenticator extends
AbstractAuthenticator implements Authentic
}
@PostConstruct
- public void init() {
- this.keystoreType = userConfiguration.getString(
UserConfigurationKeys.AUTHENTICATION_JWT_KEYSTORETYPE );
- this.sigAlg = userConfiguration.getString(
UserConfigurationKeys.AUTHENTICATION_JWT_SIGALG );
- if ( this.sigAlg.startsWith( "HS" ) ) {
- this.symmetricAlg = true;
- } else {
- this.symmetricAlg = false;
+ public void init( )
+ {
+ this.keyCounter = new AtomicLong( System.currentTimeMillis( ) );
+ this.keystoreType = userConfiguration.getString(
AUTHENTICATION_JWT_KEYSTORETYPE, AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY );
+ this.fileStore = this.keystoreType.equals(
AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE );
+ this.signatureAlgorithm = userConfiguration.getString(
AUTHENTICATION_JWT_SIGALG, AUTHENTICATION_JWT_SIGALG_HS384 );
+ this.maxInMemoryKeys = userConfiguration.getInt(
AUTHENTICATION_JWT_MAX_KEYS, 5 );
+ secretKey = new LinkedHashMap<Long, SecretKey>( )
+ {
+ @Override
+ protected boolean removeEldestEntry( Map.Entry eldest )
+ {
+ return size( ) > maxInMemoryKeys;
+ }
+ };
+ keyPair = new LinkedHashMap<Long, KeyPair>( )
+ {
+ @Override
+ protected boolean removeEldestEntry( Map.Entry eldest )
+ {
+ return size( ) > maxInMemoryKeys;
+ }
+ };
+
+
+ this.symmetricAlgorithm = this.signatureAlgorithm.startsWith( "HS" );
+
+ if ( this.fileStore )
+ {
+ String file = userConfiguration.getString(
AUTHENTICATION_JWT_KEYFILE, DEFAULT_KEYFILE );
+ this.keystoreFilePath = Paths.get( file ).toAbsolutePath( );
+ handleKeyfile( );
+ }
+ else
+ {
+ // In memory key store is the default
+ addNewKey( );
+ }
+ this.parser = Jwts.parserBuilder( )
+ .setSigningKeyResolver( getResolver( ) )
+ .requireIssuer( ISSUER )
+ .build( );
+
+ lifetime = Duration.ofMillis( Long.parseLong(
userConfiguration.getString( AUTHENTICATION_JWT_LIFETIME_MS, DEFAULT_LIFETIME )
) );
+ }
+
+ private void addNewSecretKey( Long id, SecretKey key )
+ {
+ lock.writeLock( ).lock( );
+ try
+ {
+ this.secretKey.put( id, key );
+ }
+ finally
+ {
+ lock.writeLock( ).unlock( );
+ }
+ }
+
+ private void addNewKeyPair( Long id, KeyPair pair )
+ {
+ lock.writeLock( ).lock( );
+ try
+ {
+ this.keyPair.put( id, pair );
+ }
+ finally
+ {
+ lock.writeLock( ).unlock( );
+ }
+ }
+
+ private Long addNewKey( )
+ {
+ final Long id = keyCounter.incrementAndGet( );
+ if ( this.symmetricAlgorithm )
+ {
+ addNewSecretKey( id, createNewSecretKey( this.signatureAlgorithm )
);
+ }
+ else
+ {
+ addNewKeyPair( id, createNewKeyPair( this.signatureAlgorithm ) );
+ }
+ return id;
+ }
+
+ private SecretKey getSecretKey( Long id )
+ {
+ lock.readLock( ).lock( );
+ try
+ {
+ return this.secretKey.get( id );
}
- if
(this.keystoreType.equals(UserConfigurationKeys.AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY))
+ finally
{
- if ( this.symmetricAlg )
+ lock.readLock( ).unlock( );
+ }
+ }
+
+ private KeyPair getKeyPair( Long id )
+ {
+ lock.readLock( ).lock( );
+ try
+ {
+ return this.keyPair.get( id );
+ }
+ finally
+ {
+ lock.readLock( ).unlock( );
+ }
+ }
+
+ private void handleKeyfile( )
+ {
+ if ( !Files.exists( this.keystoreFilePath ) )
+ {
+ final Long keyId = addNewKey( );
+ if ( this.symmetricAlgorithm )
+ {
+ try
+ {
+ writeSecretKey( this.keystoreFilePath, keyId,
getSecretKey( keyId ) );
+ }
+ catch ( IOException e )
+ {
+ log.error( "Could not write Jwt key file {}: {}",
this.keystoreFilePath, e.getMessage( ), e );
+ log.warn( "Switching to in memory key handling " );
+ this.fileStore = false;
+ }
+ }
+ else
+ {
+ try
+ {
+ writeKeyPair( this.keystoreFilePath, keyId, getKeyPair(
keyId ) );
+ }
+ catch ( IOException e )
+ {
+ log.error( "Could not write Jwt key file {}: {}",
this.keystoreFilePath, e.getMessage( ), e );
+ log.warn( "Switching to in memory key handling " );
+ this.fileStore = false;
+ }
+ }
+ }
+ else
+ {
+ if ( this.symmetricAlgorithm )
+ {
+ try
+ {
+ final KeyHolder key = loadKeyFromFile(
this.keystoreFilePath );
+ keyCounter.set( key.getId() );
+ addNewSecretKey( key.getId(), key.getSecretKey() );
+ }
+ catch ( IOException e )
+ {
+ log.error( "Could not read Jwt key file {}: {}",
this.keystoreFilePath, e.getMessage( ), e );
+ log.warn( "Switching to in memory key handling " );
+ this.fileStore = false;
+ addNewKey( );
+ }
+ }
+ else
{
- this.key = createNewSecretKey( this.sigAlg );
- } else {
- KeyPair pair = createNewKeyPair( this.sigAlg );
- this.key = pair.getPrivate( );
- this.publicKey = pair.getPublic( );
+ try
+ {
+ final KeyHolder pair = loadPairFromFile(
this.keystoreFilePath );
+ keyCounter.set( pair.getId() );
+ addNewKeyPair( pair.getId(), pair.getKeyPair() );
+ }
+ catch ( Exception e )
+ {
+ log.error( "Could not read Jwt key file {}: {}",
this.keystoreFilePath, e.getMessage( ), e );
+ log.warn( "Switching to in memory key handling " );
+ this.fileStore = false;
+ addNewKey( );
+ }
}
}
}
- private SecretKey createNewSecretKey( String sigAlg) {
- return Keys.secretKeyFor( SignatureAlgorithm.forName( sigAlg ));
+ private SecretKey createNewSecretKey( String sigAlg )
+ {
+ return Keys.secretKeyFor( SignatureAlgorithm.forName( sigAlg ) );
}
- private KeyPair createNewKeyPair(String sigAlg) {
- return Keys.keyPairFor( SignatureAlgorithm.forName( sigAlg ));
+ private KeyPair createNewKeyPair( String sigAlg )
+ {
+ return Keys.keyPairFor( SignatureAlgorithm.forName( sigAlg ) );
}
- private SecretKey loadKeyFromFile(Path filePath) throws IOException
+ private KeyHolder loadKeyFromFile( Path filePath ) throws IOException
{
- if ( Files.exists( filePath )) {
+ if ( Files.exists( filePath ) )
+ {
Properties props = new Properties( );
- try ( InputStream in = Files.newInputStream( filePath )) {
+ try ( InputStream in = Files.newInputStream( filePath ) )
+ {
props.loadFromXML( in );
}
String algorithm = props.getProperty( PROP_PRIV_ALG ).trim( );
String secretKey = props.getProperty( PROP_PRIVATEKEY ).trim( );
- byte[] keyData = Base64.getDecoder( ).decode( secretKey.getBytes()
);
- return new SecretKeySpec(keyData, algorithm);
- } else {
- throw new RuntimeException( "Could not load keyfile from path " );
+ Long keyId;
+ try {
+ keyId = Long.valueOf( props.getProperty( PROP_KEYID ) );
+ } catch (NumberFormatException e) {
+ keyId = keyCounter.incrementAndGet( );
+ }
+ byte[] keyData = Base64.getDecoder( ).decode( secretKey.getBytes(
) );
+ return new KeyHolder( keyId, new SecretKeySpec( keyData, algorithm
) );
+ }
+ else
+ {
+ throw new FileNotFoundException( "Keyfile does not exist " +
filePath );
}
}
- private KeyPair loadPairFromFile(Path filePath) throws IOException,
NoSuchAlgorithmException, InvalidKeySpecException
+ private KeyHolder loadPairFromFile( Path filePath ) throws IOException,
NoSuchAlgorithmException, InvalidKeySpecException
{
- if (Files.exists( filePath )) {
+ if ( Files.exists( filePath ) )
+ {
Properties props = new Properties( );
- try ( InputStream in = Files.newInputStream( filePath )) {
+ try ( InputStream in = Files.newInputStream( filePath ) )
+ {
props.loadFromXML( in );
}
String algorithm = props.getProperty( PROP_PRIV_ALG ).trim( );
String secretKeyBase64 = props.getProperty( PROP_PRIVATEKEY
).trim( );
String publicKeyBase64 = props.getProperty( PROP_PUBLICKEY ).trim(
);
+ Long keyId;
+ try {
+ keyId = Long.valueOf( props.getProperty( PROP_KEYID ) );
+ } catch (NumberFormatException e) {
+ keyId = keyCounter.incrementAndGet( );
+ }
byte[] privateBytes = Base64.getDecoder( ).decode( secretKeyBase64
);
byte[] publicBytes = Base64.getDecoder( ).decode( publicKeyBase64
);
@@ -156,13 +405,15 @@ public class JwtAuthenticator extends
AbstractAuthenticator implements Authentic
PrivateKey privateKey = KeyFactory.getInstance( algorithm
).generatePrivate( privateSpec );
PublicKey publicKey = KeyFactory.getInstance( algorithm
).generatePublic( publicSpec );
- return new KeyPair( publicKey, privateKey );
- } else {
- throw new RuntimeException( "Could not load key file from " +
filePath );
+ return new KeyHolder( keyId, new KeyPair( publicKey, privateKey )
);
+ }
+ else
+ {
+ throw new FileNotFoundException( "Keyfile does not exist " +
filePath );
}
}
- private void writeSecretKey(Path filePath, SecretKey key) throws
IOException
+ private void writeSecretKey( Path filePath, Long id, Key key ) throws
IOException
{
log.info( "Writing secret key algorithm=" + key.getAlgorithm( ) + ",
format=" + key.getFormat( ) + " to file " + filePath );
Properties props = new Properties( );
@@ -171,29 +422,39 @@ public class JwtAuthenticator extends
AbstractAuthenticator implements Authentic
{
props.setProperty( PROP_PRIV_FORMAT, key.getFormat( ) );
}
- props.setProperty( PROP_PRIVATEKEY, String.valueOf( Base64.getEncoder(
).encode( key.getEncoded( ) ) ) );
+ props.setProperty( PROP_KEYID, id.toString() );
+ props.setProperty( PROP_PRIVATEKEY, Base64.getEncoder(
).encodeToString( key.getEncoded( ) ) );
try ( OutputStream out = Files.newOutputStream( filePath ) )
{
props.storeToXML( out, "Key for JWT signing" );
}
try
{
- Files.setPosixFilePermissions( filePath,
PosixFilePermissions.fromString( "600" ) );
- } catch (Exception e) {
- log.error( "Could not set file permissions for " + filePath );
+ Files.setPosixFilePermissions( filePath,
PosixFilePermissions.fromString( "rw-------" ) );
+ }
+ catch ( Exception e )
+ {
+ log.error( "Could not set file permissions for {}: {}", filePath,
e.getMessage( ), e );
}
}
- private void writeKeyPair(Path filePath, PrivateKey privateKey, PublicKey
publicKey) {
+ private void writeKeyPair( Path filePath, Long id, KeyPair keyPair )
throws IOException
+ {
+ PrivateKey privateKey = keyPair.getPrivate( );
+ PublicKey publicKey = keyPair.getPublic( );
+
log.info( "Writing private key algorithm=" + privateKey.getAlgorithm(
) + ", format=" + privateKey.getFormat( ) + " to file " + filePath );
log.info( "Writing public key algorithm=" + publicKey.getAlgorithm( )
+ ", format=" + publicKey.getFormat( ) + " to file " + filePath );
Properties props = new Properties( );
props.setProperty( PROP_PRIV_ALG, privateKey.getAlgorithm( ) );
- if (privateKey.getFormat()!=null) {
+ if ( privateKey.getFormat( ) != null )
+ {
props.setProperty( PROP_PRIV_FORMAT, privateKey.getFormat( ) );
}
+ props.setProperty( PROP_KEYID, id.toString( ) );
props.setProperty( PROP_PUB_ALG, publicKey.getAlgorithm( ) );
- if (publicKey.getFormat()!=null) {
+ if ( publicKey.getFormat( ) != null )
+ {
props.setProperty( PROP_PUB_FORMAT, publicKey.getFormat( ) );
}
PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(
privateKey.getEncoded( ) );
@@ -201,23 +462,283 @@ public class JwtAuthenticator extends
AbstractAuthenticator implements Authentic
props.setProperty( PROP_PRIVATEKEY, Base64.getEncoder(
).encodeToString( privateSpec.getEncoded( ) ) );
props.setProperty( PROP_PUBLICKEY, Base64.getEncoder(
).encodeToString( publicSpec.getEncoded( ) ) );
+ try ( OutputStream out = Files.newOutputStream( filePath ) )
+ {
+ props.storeToXML( out, "Key pair for JWT signing" );
+ }
+ try
+ {
+ Files.setPosixFilePermissions( filePath,
PosixFilePermissions.fromString( "rw-------" ) );
+ }
+ catch ( Exception e )
+ {
+ log.error( "Could not set file permissions for {}: {}", filePath,
e.getMessage( ), e );
+ }
}
@Override
public boolean supportsDataSource( AuthenticationDataSource source )
{
- return (source instanceof TokenBasedAuthenticationDataSource);
+ return ( source instanceof TokenBasedAuthenticationDataSource );
}
@Override
- public AuthenticationResult authenticate( AuthenticationDataSource source
) throws AccountLockedException, AuthenticationException,
MustChangePasswordException
+ public AuthenticationResult authenticate( AuthenticationDataSource source
) throws AuthenticationException
{
- if (source instanceof TokenBasedAuthenticationDataSource ) {
+ if ( source instanceof TokenBasedAuthenticationDataSource )
+ {
TokenBasedAuthenticationDataSource tSource =
(TokenBasedAuthenticationDataSource) source;
- return null;
- } else {
+ String jwt = tSource.getToken( );
+ AuthenticationResult result;
+ try
+ {
+ String subject = verify( jwt );
+ result = new AuthenticationResult( true, subject, null );
+ } catch (AuthenticationException e) {
+ result = new AuthenticationResult( false,
source.getUsername(), e );
+ }
+ return result;
+ }
+ else
+ {
throw new AuthenticationException( "The provided authentication
source is not suitable for this authenticator" );
}
}
+
+ /**
+ * Creates a new signing key and uses this for new tokens. It will keep
{@link #maxInMemoryKeys} keys in the
+ * list for jwt verification.
+ */
+ public Long renewSigningKey( )
+ {
+ final Long id = addNewKey( );
+ if (this.fileStore)
+ {
+ if ( this.symmetricAlgorithm )
+ {
+ try
+ {
+ writeSecretKey( this.keystoreFilePath, id, getSecretKey(
id ) );
+ }
+ catch ( IOException e )
+ {
+ log.error( "Could not write to keyfile {}: {}",
this.keystoreFilePath, e.getMessage( ), e );
+ }
+ }
+ else
+ {
+ try
+ {
+ writeKeyPair( this.keystoreFilePath, id, getKeyPair( id )
);
+ }
+ catch ( IOException e )
+ {
+ log.error( "Could not write to keyfile {}: {}",
this.keystoreFilePath, e.getMessage( ), e );
+ }
+ }
+ }
+ return id;
+ }
+
+ private static class KeyHolder {
+ final Long id;
+ final SecretKey secretKey;
+ final KeyPair keyPair;
+
+ KeyHolder(Long id, SecretKey key) {
+ this.id = id;
+ this.secretKey = key;
+ this.keyPair = null;
+ }
+ KeyHolder(Long id, KeyPair key) {
+ this.id = id;
+ this.secretKey = null;
+ this.keyPair = key;
+ }
+
+ public Long getId( )
+ {
+ return id;
+ }
+
+ public SecretKey getSecretKey( )
+ {
+ return secretKey;
+ }
+
+ public KeyPair getKeyPair( )
+ {
+ return keyPair;
+ }
+
+ public Key getSignerKey() {
+ return keyPair != null ? this.keyPair.getPrivate( ) :
this.secretKey;
+ }
+ }
+
+ private KeyHolder getSignerKey() {
+ final Long id = keyCounter.get( );
+ if (this.symmetricAlgorithm) {
+ return new KeyHolder( id, getSecretKey( id ) );
+ } else {
+ return new KeyHolder( id, getKeyPair( id ) );
+ }
+ }
+
+ /**
+ * Creates a token for the given user id. The token contains the following
data:
+ * <ul>
+ * <li>the userid as subject</li>
+ * <li>a issuer archiva.apache.org/redback</li>
+ * <li>a id header with the key id</li>
+ * </ul>the user id as subject.
+ *
+ * @param userId the user identifier to set as subject
+ * @return the token string
+ */
+ public Token generateToken( String userId )
+ {
+ final KeyHolder signerKey = getSignerKey( );
+ Instant now = Instant.now( );
+ Instant expiration = now.plus( lifetime );
+ final String token = Jwts.builder( )
+ .setSubject( userId )
+ .setIssuer( ISSUER )
+ .setIssuedAt( Date.from( now ) )
+ .setExpiration( Date.from( expiration ) )
+ .setHeaderParam( JwsHeader.KEY_ID, signerKey.getId( ).toString( ) )
+ .signWith( signerKey.getSignerKey( ) ).compact( );
+ TokenData metadata = new SimpleTokenData( userId, lifetime.toMillis(
), 0 );
+ return new StringToken( token, metadata );
+ }
+
+ /**
+ * Allows to renew a token based on the origin token. If the presented
<code>origin</code>
+ * is valid, a new token with refreshed expiration time will be returned.
+ *
+ * @param origin the origin token
+ * @return the newly created token
+ * @throws AuthenticationException if the given origin token is not valid
+ */
+ public Token renewToken(String origin) throws AuthenticationException {
+ try
+ {
+ Jws<Claims> signature = this.parser.parseClaimsJws( origin );
+ return generateToken( signature.getBody( ).getSubject( ) );
+ } catch (JwtException e) {
+ throw new AuthenticationException( "Could not renew the token " +
e.getMessage( ) );
+ }
+ }
+
+ /**
+ * Parses the given token and returns the JWS metadata stored in the token.
+ *
+ * @param token the token string
+ * @return the parsed data
+ * @throws JwtException if the token data is not valid anymore
+ */
+ public Jws<Claims> parseToken( String token) throws JwtException {
+ return parser.parseClaimsJws( token );
+ }
+
+ /**
+ * Verifies the given JWT Token and returns the stored subject, if
successful
+ * If the verification failed a AuthenticationException is thrown.
+ * @param token the JWT representation
+ * @return the subject of the JWT
+ * @throws AuthenticationException if the verification failed
+ */
+ public String verify( String token ) throws AuthenticationException
+ {
+ try
+ {
+ Jws<Claims> signature = this.parser.parseClaimsJws( token );
+ String subject = signature.getBody( ).getSubject( );
+ if ( StringUtils.isEmpty( subject ) )
+ {
+ throw new AuthenticationException( "Subject in JWT is empty" );
+ }
+ return subject;
+ }
+ catch ( JwtException e )
+ {
+ throw new AuthenticationException( e.getMessage( ), e );
+ }
+ }
+
+ /**
+ * Removes all signing keys and creates a new one.
+ */
+ public void revokeSigningKeys() {
+ lock.writeLock( ).lock( );
+ try {
+ this.secretKey.clear();
+ this.keyPair.clear();
+ renewSigningKey( );
+ } finally
+ {
+ lock.writeLock( ).unlock( );
+ }
+ }
+
+ private SigningKeyResolver getResolver( )
+ {
+ return this.resolver;
+ }
+
+ public boolean usesSymmetricAlgorithm( )
+ {
+ return symmetricAlgorithm;
+ }
+
+ public String getSignatureAlgorithm( )
+ {
+ return signatureAlgorithm;
+ }
+
+ public String getKeystoreType( )
+ {
+ return keystoreType;
+ }
+
+ public Path getKeystoreFilePath( )
+ {
+ return keystoreFilePath;
+ }
+
+ public int getMaxInMemoryKeys( )
+ {
+ return maxInMemoryKeys;
+ }
+
+ public int getCurrentKeyListSize() {
+ if (symmetricAlgorithm) {
+ return secretKey.size( );
+ } else {
+ return keyPair.size( );
+ }
+ }
+
+ public Long getCurrentKeyId() {
+ return keyCounter.get( );
+ }
+
+ public Duration getTokenLifetime() {
+ return this.lifetime;
+ }
+
+ public void setTokenLifetime(Duration lifetime) {
+ this.lifetime = lifetime;
+ }
+
+ public UserConfiguration getUserConfiguration( )
+ {
+ return userConfiguration;
+ }
+
+ public void setUserConfiguration( UserConfiguration userConfiguration )
+ {
+ this.userConfiguration = userConfiguration;
+ }
}
diff --git
a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/resources/META-INF/spring-context.xml
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/resources/META-INF/spring-context.xml
new file mode 100644
index 0000000..83d3757
--- /dev/null
+++
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/resources/META-INF/spring-context.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you 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.
+ -->
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans
+ http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
+ http://www.springframework.org/schema/context
+
http://www.springframework.org/schema/context/spring-context-3.0.xsd"
+ default-lazy-init="true">
+
+ <context:annotation-config />
+ <context:component-scan
+ base-package="org.apache.archiva.redback.authentication.jwt"/>
+
+</beans>
\ No newline at end of file
diff --git
a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/AbstractJwtTest.java
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/AbstractJwtTest.java
new file mode 100644
index 0000000..f7b16c4
--- /dev/null
+++
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/AbstractJwtTest.java
@@ -0,0 +1,241 @@
+package org.apache.archiva.redback.authentication.jwt;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.JwsHeader;
+import org.apache.archiva.components.registry.Registry;
+import org.apache.archiva.components.registry.RegistryException;
+import
org.apache.archiva.components.registry.commons.CommonsConfigurationRegistry;
+import org.apache.archiva.redback.authentication.AuthenticationException;
+import org.apache.archiva.redback.authentication.AuthenticationResult;
+import
org.apache.archiva.redback.authentication.PasswordBasedAuthenticationDataSource;
+import org.apache.archiva.redback.authentication.Token;
+import
org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource;
+import org.apache.archiva.redback.configuration.DefaultUserConfiguration;
+import org.apache.archiva.redback.configuration.UserConfiguration;
+import org.apache.archiva.redback.configuration.UserConfigurationException;
+import org.apache.commons.configuration2.BaseConfiguration;
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.configuration2.builder.BasicConfigurationBuilder;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * @author Martin Stockhammer <[email protected]>
+ */
+public abstract class AbstractJwtTest
+{
+ protected JwtAuthenticator jwtAuthenticator;
+ protected DefaultUserConfiguration configuration;
+ protected CommonsConfigurationRegistry registry;
+ protected BaseConfiguration saveConfig;
+
+ protected void init( Map<String, String> parameters) throws
UserConfigurationException, RegistryException
+ {
+ this.registry = new CommonsConfigurationRegistry( );
+ String baseDir = System.getProperty( "basedir", "" );
+ if ( !StringUtils.isEmpty( baseDir ) && !StringUtils.endsWith(baseDir,
"/" ) )
+ {
+ baseDir = baseDir + "/";
+ }
+ this.registry.setInitialConfiguration( "<configuration>\n" +
+ " <system/>\n" +
+ " <properties
fileName=\""+baseDir+"src/test/resources/security.properties\"
config-optional=\"true\"\n" +
+ "
config-at=\"org.apache.archiva.redback\"/>\n" +
+ " </configuration>" );
+ this.registry.initialize();
+ this.saveConfig = new BaseConfiguration( );
+ this.registry.addConfiguration( this.saveConfig, "save",
"org.apache.archiva.redback" );
+ for (Map.Entry<String, String> entry : parameters.entrySet())
+ {
+ saveConfig.setProperty( entry.getKey( ), entry.getValue( ) );
+ }
+
+ this.configuration = new DefaultUserConfiguration( );
+ this.configuration.setRegistry( registry );
+ this.configuration.initialize();
+
+ jwtAuthenticator = new JwtAuthenticator( );
+ jwtAuthenticator.setUserConfiguration( configuration );
+ jwtAuthenticator.init( );
+ }
+
+ @Test
+ void getId( )
+ {
+ assertEquals( "JwtAuthenticator", jwtAuthenticator.getId( ) );
+ }
+
+ @Test
+ void supportsDataSource( )
+ {
+ assertTrue( jwtAuthenticator.supportsDataSource( new
TokenBasedAuthenticationDataSource( ) ) );
+ assertFalse( jwtAuthenticator.supportsDataSource( new
PasswordBasedAuthenticationDataSource( ) ) );
+ }
+
+
+ @Test
+ void generateToken( )
+ {
+ Token token = jwtAuthenticator.generateToken( "frodo" );
+ assertNotNull( token );
+ assertTrue( token.getData( ).length( ) > 0 );
+ Jws<Claims> parsed = jwtAuthenticator.parseToken( token.getData( ) );
+ assertNotNull( parsed.getHeader( ).get( JwsHeader.KEY_ID ) );
+ assertNotNull( token.getMetadata( ).created( ) );
+ try
+ {
+ Thread.sleep( 2 );
+ }
+ catch ( InterruptedException e )
+ {
+ //
+ }
+
+ assertTrue( Instant.now( ).isAfter( token.getMetadata( ).created( ) )
);
+ assertTrue( Instant.now( ).isBefore( token.getMetadata( ).validBefore(
) ) );
+ }
+
+
+ @Test
+ void authenticate( )
+ {
+ }
+
+ @Test
+ void renewSigningKey( )
+ {
+
+ assertEquals( 5, jwtAuthenticator.getMaxInMemoryKeys( ) );
+ assertEquals( 1, jwtAuthenticator.getCurrentKeyListSize( ) );
+ jwtAuthenticator.renewSigningKey( );
+ assertEquals( 2, jwtAuthenticator.getCurrentKeyListSize( ) );
+ jwtAuthenticator.renewSigningKey( );
+ assertEquals( 3, jwtAuthenticator.getCurrentKeyListSize( ) );
+ jwtAuthenticator.renewSigningKey( );
+ assertEquals( 4, jwtAuthenticator.getCurrentKeyListSize( ) );
+ jwtAuthenticator.renewSigningKey( );
+ assertEquals( 5, jwtAuthenticator.getCurrentKeyListSize( ) );
+ jwtAuthenticator.renewSigningKey( );
+ assertEquals( 5, jwtAuthenticator.getCurrentKeyListSize( ) );
+ jwtAuthenticator.renewSigningKey( );
+ assertEquals( 5, jwtAuthenticator.getCurrentKeyListSize( ) );
+
+
+ }
+
+ @Test
+ void verify( ) throws AuthenticationException
+ {
+ Token token = jwtAuthenticator.generateToken( "frodo_baggins" );
+ assertEquals( "frodo_baggins", jwtAuthenticator.verify( token.getData(
) ) );
+ }
+
+ @Test
+ void usesSymmetricAlgorithm( )
+ {
+ assertTrue( jwtAuthenticator.usesSymmetricAlgorithm( ) );
+ }
+
+ @Test
+ void getSignatureAlgorithm( )
+ {
+ assertEquals( "HS384", jwtAuthenticator.getSignatureAlgorithm( ) );
+ }
+
+ @Test
+ void getMaxInMemoryKeys( )
+ {
+ assertEquals( 5, jwtAuthenticator.getMaxInMemoryKeys( ) );
+ }
+
+ @Order( 0 )
+ @Test
+ void getCurrentKeyListSize( )
+ {
+ assertEquals( 1, jwtAuthenticator.getCurrentKeyListSize( ) );
+ }
+
+ @Test
+ void invalidKeySignature() throws AuthenticationException
+ {
+ Token token = jwtAuthenticator.generateToken( "samwise_gamgee" );
+ assertEquals( "samwise_gamgee", jwtAuthenticator.verify(
token.getData( ) ) );
+ jwtAuthenticator.revokeSigningKeys( );
+ assertThrows( AuthenticationException.class, ( ) -> {
+ jwtAuthenticator.verify( token.getData( ) );
+ } );
+ }
+
+
+ @Test
+ void invalidKeyDate( )
+ {
+ Duration lifetime = jwtAuthenticator.getTokenLifetime( );
+ try
+ {
+ jwtAuthenticator.setTokenLifetime( Duration.ofNanos( 0 ) );
+ Token token = jwtAuthenticator.generateToken( "samwise_gamgee" );
+ assertThrows( AuthenticationException.class, ( ) -> {
+ jwtAuthenticator.verify( token.getData( ) );
+ } );
+ } finally
+ {
+ jwtAuthenticator.setTokenLifetime( lifetime );
+ }
+
+ }
+
+ @Test
+ void validAuthenticate() throws AuthenticationException
+ {
+ Token token = jwtAuthenticator.generateToken( "bilbo_baggins" );
+ TokenBasedAuthenticationDataSource source = new
TokenBasedAuthenticationDataSource( );
+ source.setPrincipal( "bilbo_baggins" );
+ source.setToken( token.getData() );
+ AuthenticationResult result = jwtAuthenticator.authenticate( source );
+ assertNotNull( result );
+ assertTrue( result.isAuthenticated( ) );
+ assertEquals( "bilbo_baggins", result.getPrincipal( ) );
+ }
+
+ @Test
+ void invalidAuthenticate() throws AuthenticationException
+ {
+ TokenBasedAuthenticationDataSource source = new
TokenBasedAuthenticationDataSource( );
+ source.setPrincipal( "bilbo_baggins" );
+ source.setToken( "invalidToken" );
+ AuthenticationResult result = jwtAuthenticator.authenticate( source );
+ assertNotNull( result );
+ assertFalse( result.isAuthenticated( ) );
+ assertEquals( "bilbo_baggins", result.getPrincipal( ) );
+ }
+
+
+}
diff --git
a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedPublicKeyTest.java
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedPublicKeyTest.java
new file mode 100644
index 0000000..63a0d85
--- /dev/null
+++
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedPublicKeyTest.java
@@ -0,0 +1,120 @@
+package org.apache.archiva.redback.authentication.jwt;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import org.apache.archiva.components.registry.RegistryException;
+import org.apache.archiva.redback.configuration.UserConfigurationException;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.apache.archiva.redback.configuration.UserConfigurationKeys.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * @author Martin Stockhammer <[email protected]>
+ */
+class JwtAuthenticatorFilebasedPublicKeyTest extends AbstractJwtTest
+{
+
+ @BeforeEach
+ void init() throws RegistryException, UserConfigurationException
+ {
+ Map<String, String> params = new HashMap<>();
+ params.put( AUTHENTICATION_JWT_KEYSTORETYPE,
AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE );
+ params.put( AUTHENTICATION_JWT_SIGALG, AUTHENTICATION_JWT_SIGALG_RS256
);
+ super.init( params );
+ }
+
+ @AfterEach
+ void clean() {
+ Path file = Paths.get( jwtAuthenticator.DEFAULT_KEYFILE
).toAbsolutePath();
+ try
+ {
+ Files.deleteIfExists( file );
+ }
+ catch ( IOException e )
+ {
+ try
+ {
+ Files.move( file, file.getParent().resolve(
file.getFileName().toString()+"." + System.currentTimeMillis( ) ) );
+ }
+ catch ( IOException ioException )
+ {
+ ioException.printStackTrace();
+ }
+ //
+ }
+ }
+
+ @Test
+ @Override
+ void usesSymmetricAlgorithm( )
+ {
+ assertFalse( jwtAuthenticator.usesSymmetricAlgorithm( ) );
+ }
+
+ @Test
+ @Override
+ void getSignatureAlgorithm( )
+ {
+ assertEquals( "RS256", jwtAuthenticator.getSignatureAlgorithm( ) );
+ }
+
+ @Test
+ void keyFileExists() throws IOException
+ {
+ Path path = jwtAuthenticator.getKeystoreFilePath( );
+ assertNotNull( path );
+ assertTrue( Files.exists( path ) );
+ Properties props = new Properties( );
+ try ( InputStream in = Files.newInputStream( path ) )
+ {
+ props.loadFromXML( in );
+ assertTrue( StringUtils.isNotEmpty( props.getProperty(
JwtAuthenticator.PROP_PRIV_ALG ) ) );
+ assertTrue( StringUtils.isNotEmpty( props.getProperty(
JwtAuthenticator.PROP_PRIVATEKEY ) ) );
+ }
+ }
+
+ @Test
+ void getKeystoreType( )
+ {
+ assertEquals( "plainfile", jwtAuthenticator.getKeystoreType( ) );
+ }
+
+ @Test
+ void getKeystoreFilePath( )
+ {
+ assertNotNull( jwtAuthenticator.getKeystoreFilePath( ) );
+ assertEquals( "jwt-key.xml", jwtAuthenticator.getKeystoreFilePath(
).getFileName().toString() );
+ }
+
+}
\ No newline at end of file
diff --git
a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedTest.java
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedTest.java
new file mode 100644
index 0000000..ecbce68
--- /dev/null
+++
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorFilebasedTest.java
@@ -0,0 +1,107 @@
+package org.apache.archiva.redback.authentication.jwt;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import org.apache.archiva.components.registry.RegistryException;
+import org.apache.archiva.redback.configuration.UserConfigurationException;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import static
org.apache.archiva.redback.configuration.UserConfigurationKeys.AUTHENTICATION_JWT_KEYSTORETYPE;
+import static
org.apache.archiva.redback.configuration.UserConfigurationKeys.AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * @author Martin Stockhammer <[email protected]>
+ */
+@TestMethodOrder( MethodOrderer.OrderAnnotation.class )
+class JwtAuthenticatorFilebasedTest extends AbstractJwtTest
+{
+
+ @BeforeEach
+ void init() throws RegistryException, UserConfigurationException
+ {
+ Map<String, String> params = new HashMap<>();
+ params.put( AUTHENTICATION_JWT_KEYSTORETYPE,
AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE );
+ super.init( params );
+ }
+
+ @AfterEach
+ void clean() {
+ Path file = Paths.get( jwtAuthenticator.DEFAULT_KEYFILE
).toAbsolutePath();
+ try
+ {
+ Files.deleteIfExists( file );
+ }
+ catch ( IOException e )
+ {
+ try
+ {
+ Files.move( file, file.getParent().resolve(
file.getFileName().toString()+"." + System.currentTimeMillis( ) ) );
+ }
+ catch ( IOException ioException )
+ {
+ ioException.printStackTrace();
+ }
+ //
+ }
+ }
+
+ @Test
+ void keyFileExists() throws IOException
+ {
+ Path path = jwtAuthenticator.getKeystoreFilePath( );
+ assertNotNull( path );
+ assertTrue( Files.exists( path ) );
+ Properties props = new Properties( );
+ try ( InputStream in = Files.newInputStream( path ) )
+ {
+ props.loadFromXML( in );
+ assertTrue( StringUtils.isNotEmpty( props.getProperty(
JwtAuthenticator.PROP_PRIV_ALG ) ) );
+ assertTrue( StringUtils.isNotEmpty( props.getProperty(
JwtAuthenticator.PROP_PRIVATEKEY ) ) );
+ }
+ }
+
+ @Test
+ void getKeystoreType( )
+ {
+ assertEquals( "plainfile", jwtAuthenticator.getKeystoreType( ) );
+ }
+
+ @Test
+ void getKeystoreFilePath( )
+ {
+ assertNotNull( jwtAuthenticator.getKeystoreFilePath( ) );
+ assertEquals( "jwt-key.xml", jwtAuthenticator.getKeystoreFilePath(
).getFileName().toString() );
+ }
+
+}
\ No newline at end of file
diff --git
a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorMemorybasedTest.java
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorMemorybasedTest.java
new file mode 100644
index 0000000..fa876f7
--- /dev/null
+++
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticatorMemorybasedTest.java
@@ -0,0 +1,75 @@
+package org.apache.archiva.redback.authentication.jwt;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import org.apache.archiva.components.registry.RegistryException;
+import org.apache.archiva.redback.configuration.UserConfigurationException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static
org.apache.archiva.redback.configuration.UserConfigurationKeys.AUTHENTICATION_JWT_KEYSTORETYPE;
+import static
org.apache.archiva.redback.configuration.UserConfigurationKeys.AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * @author Martin Stockhammer <[email protected]>
+ */
+@TestMethodOrder( MethodOrderer.OrderAnnotation.class )
+class JwtAuthenticatorMemorybasedTest extends AbstractJwtTest
+{
+ @BeforeEach
+ void init() throws RegistryException, UserConfigurationException
+ {
+ Map<String, String> params = new HashMap<>();
+ params.put( AUTHENTICATION_JWT_KEYSTORETYPE,
AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY );
+ super.init( params );
+ }
+
+
+ @Test
+ void authenticate( )
+ {
+ }
+
+ @Test
+ void getKeystoreType( )
+ {
+ assertEquals( "memory", jwtAuthenticator.getKeystoreType( ) );
+ }
+
+ @Test
+ void getKeystoreFilePath( )
+ {
+ assertNull( jwtAuthenticator.getKeystoreFilePath( ) );
+ }
+
+ @Test
+ void getMaxInMemoryKeys( )
+ {
+ assertEquals( 5, jwtAuthenticator.getMaxInMemoryKeys( ) );
+ }
+
+
+}
\ No newline at end of file
diff --git
a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/resources/log4j2-test.xml
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/resources/log4j2-test.xml
new file mode 100644
index 0000000..d3c8816
--- /dev/null
+++
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/resources/log4j2-test.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you 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.
+ -->
+<configuration>
+ <appenders>
+ <Console name="console" target="SYSTEM_OUT">
+ <PatternLayout pattern="[%t] %-5p %c %x - %m%n"/>
+ </Console>
+ </appenders>
+ <loggers>
+ <logger name="org.apache.archiva" level="info"/>
+ <logger name="org.apache.archiva.redback.authentication" level="info"
/>
+
+ <root level="error" includeLocation="true">
+ <appender-ref ref="console"/>
+ </root>
+ </loggers>
+</configuration>
+
+
diff --git
a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/resources/security.properties
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/resources/security.properties
new file mode 100644
index 0000000..c84059f
--- /dev/null
+++
b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/resources/security.properties
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+user.manager.impl=ldap
+ldap.bind.authenticator.enabled=true
+redback.default.admin=adminuser
+redback.default.guest=guest
+security.policy.password.expiration.enabled=false
diff --git
a/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/DefaultUserConfiguration.java
b/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/DefaultUserConfiguration.java
index 548d239..84a99fa 100644
---
a/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/DefaultUserConfiguration.java
+++
b/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/DefaultUserConfiguration.java
@@ -57,7 +57,7 @@ public class DefaultUserConfiguration
private Registry lookupRegistry;
- private static final String PREFIX = "org.apache.archiva.redback";
+ public static final String PREFIX = "org.apache.archiva.redback";
@Inject
@Named(value = "commons-configuration")
diff --git
a/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java
b/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java
index 1ee7c82..2cd3341 100644
---
a/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java
+++
b/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java
@@ -194,6 +194,8 @@ public interface UserConfigurationKeys
String AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY = "memory";
String AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE = "plainfile";
String AUTHENTICATION_JWT_SIGALG = "authentication.jwt.signatureAlgorithm";
+ String AUTHENTICATION_JWT_MAX_KEYS = "authentication.jwt.maxInMemoryKeys";
+
/**
* HMAC using SHA-256
*/
@@ -249,4 +251,9 @@ public interface UserConfigurationKeys
*/
String AUTHENTICATION_JWT_KEYFILE = "authentication.jwt.keyfile";
+ /**
+ * The lifetime in ms of the generated tokens.
+ */
+ String AUTHENTICATION_JWT_LIFETIME_MS = "authentication.jwt.lifetimeMs";
+
}
diff --git
a/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties
b/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties
index 3cdd1d1..90783b1 100644
---
a/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties
+++
b/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties
@@ -156,4 +156,5 @@ rest.csrffilter.disableTokenValidation=false
# Configuration for JWT authentication
authentication.jwt.keystoreType=memory
authentication.jwt.signatureAlgorithm=HS384
-authentication.jwt.keyfile=jwt-key.xml
\ No newline at end of file
+authentication.jwt.keyfile=jwt-key.xml
+authentication.jwt.maxInMemoryKeys=5
\ No newline at end of file