Adding implementation of JWT token authentication
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 @@
*/
import java.io.Serializable;
+import java.time.Duration;
+import java.time.Instant;
import java.util.Date;
/**
@@ -39,11 +41,10 @@
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 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 @@
}
@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 @@
@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/StringToken.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/StringToken.java
new file mode 100644
index 0000000..c96c4e2
--- /dev/null
+++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/StringToken.java
@@ -0,0 +1,53 @@
+package org.apache.archiva.redback.authentication;
+
+/*
+ * 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.
+ */
+
+/**
+ * Simple token implementation. This implementation is immutable.
+ *
+ * @author Martin Stockhammer <martin_s@apache.org>
+ */
+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/Token.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/Token.java
new file mode 100644
index 0000000..221a57b
--- /dev/null
+++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/Token.java
@@ -0,0 +1,34 @@
+package org.apache.archiva.redback.authentication;
+
+/*
+ * 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.
+ */
+
+/**
+ * This interface represents a token including its metadata.
+ *
+ * @author Martin Stockhammer <martin_s@apache.org>
+ */
+public interface Token
+{
+
+ String getData();
+
+ byte[] getBytes();
+
+ 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 @@
* under the License.
*/
-import java.util.Date;
+import java.time.Instant;
/**
*
@@ -41,14 +41,14 @@
*
* @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 @@
* 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.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.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 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 @@
}
@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;
- }
- if (this.keystoreType.equals(UserConfigurationKeys.AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY))
+ 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>( )
{
- if ( this.symmetricAlg )
+ @Override
+ protected boolean removeEldestEntry( Map.Entry eldest )
{
- this.key = createNewSecretKey( this.sigAlg );
- } else {
- KeyPair pair = createNewKeyPair( this.sigAlg );
- this.key = pair.getPrivate( );
- this.publicKey = pair.getPublic( );
+ 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 );
+ }
+ finally
+ {
+ 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
+ {
+ 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 KeyPair createNewKeyPair(String sigAlg) {
- return Keys.keyPairFor( SignatureAlgorithm.forName( sigAlg ));
- }
-
- private SecretKey loadKeyFromFile(Path filePath) throws IOException
+ private SecretKey createNewSecretKey( String sigAlg )
{
- if ( Files.exists( filePath )) {
+ return Keys.secretKeyFor( SignatureAlgorithm.forName( sigAlg ) );
+ }
+
+ private KeyPair createNewKeyPair( String sigAlg )
+ {
+ return Keys.keyPairFor( SignatureAlgorithm.forName( sigAlg ) );
+ }
+
+ private KeyHolder loadKeyFromFile( Path filePath ) throws IOException
+ {
+ 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 @@
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 @@
{
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 @@
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 <martin_s@apache.org>
+ */
+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 <martin_s@apache.org>
+ */
+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 <martin_s@apache.org>
+ */
+@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 <martin_s@apache.org>
+ */
+@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 @@
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 @@
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 @@
*/
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 @@
# 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