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