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 16f6040b..fc0de018 100644 --- a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java @@ -20,6 +20,8 @@ package org.apache.archiva.redback.authentication; */ import java.io.Serializable; +import java.time.Duration; +import java.time.Instant; import java.util.Date; /** @@ -39,11 +41,10 @@ public final class SimpleTokenData implements Serializable, TokenData { private static final long serialVersionUID = 5907745449771921813L; private final String user; - private final Date created; - private final Date validBefore; + private final Instant created; + private final Instant validBefore; private final long nonce; - /** * Creates a new token info instance for the given user. * The lifetime in milliseconds defines the invalidation date by @@ -55,8 +56,8 @@ public final class SimpleTokenData implements Serializable, TokenData { */ public SimpleTokenData(final String user, final long lifetime, final long nonce) { this.user=user; - this.created=new Date(); - this.validBefore =new Date(created.getTime()+lifetime); + this.created = Instant.now( ); + this.validBefore = created.plus( Duration.ofMillis( lifetime ) ); this.nonce = nonce; } @@ -66,12 +67,12 @@ public final class SimpleTokenData implements Serializable, TokenData { } @Override - public final Date created() { + public final Instant created() { return created; } @Override - public final Date validBefore() { + public final Instant validBefore() { return validBefore; } @@ -82,7 +83,7 @@ public final class SimpleTokenData implements Serializable, TokenData { @Override public boolean isValid() { - return (System.currentTimeMillis()) + */ +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 00000000..221a57b5 --- /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 + */ +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 f641f3a1..d8f9c04f 100644 --- a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java @@ -19,7 +19,7 @@ package org.apache.archiva.redback.authentication; * under the License. */ -import java.util.Date; +import java.time.Instant; /** * @@ -41,14 +41,14 @@ public interface TokenData { * * @return The creation date. */ - Date created(); + Instant created(); /** * The date after that the token is invalid. * * @return The invalidation date. */ - Date validBefore(); + Instant validBefore(); /** * The nonce that is stored in the token. diff --git a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/pom.xml b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/pom.xml index 77a6c654..b37670d2 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 @@ jjwt-jackson runtime + + + + org.apache.archiva.components.registry + archiva-components-spring-registry-commons + test + + + org.springframework + spring-test + test + 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 8ebe45f2..f9a3a322 100644 --- a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticator.java +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticator.java @@ -19,18 +19,28 @@ package org.apache.archiva.redback.authentication.jwt; * under the License. */ +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SigningKeyResolverAdapter; import io.jsonwebtoken.security.Keys; import org.apache.archiva.redback.authentication.AbstractAuthenticator; import org.apache.archiva.redback.authentication.AuthenticationDataSource; import org.apache.archiva.redback.authentication.AuthenticationException; import org.apache.archiva.redback.authentication.AuthenticationResult; import org.apache.archiva.redback.authentication.Authenticator; +import org.apache.archiva.redback.authentication.SimpleTokenData; +import org.apache.archiva.redback.authentication.StringToken; +import org.apache.archiva.redback.authentication.Token; import org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource; +import org.apache.archiva.redback.authentication.TokenData; import org.apache.archiva.redback.configuration.UserConfiguration; -import org.apache.archiva.redback.configuration.UserConfigurationKeys; -import org.apache.archiva.redback.policy.AccountLockedException; -import org.apache.archiva.redback.policy.MustChangePasswordException; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -40,11 +50,13 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import javax.inject.Inject; import javax.inject.Named; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.attribute.PosixFilePermissions; import java.security.Key; import java.security.KeyFactory; @@ -55,15 +67,39 @@ import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; +import java.time.Duration; +import java.time.Instant; import java.util.Base64; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Properties; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import static org.apache.archiva.redback.configuration.UserConfigurationKeys.*; -@Service("authenticator#jwt") +/** + * Authenticator for JWT tokens. This authenticator needs a secret key or keypair depending + * on the used algorithm for signing and verification. + * The key can be either volatile in memory, which means a new one is created, with each + * start of the service. Or it can be stored in a file. + * If this service is running in a cluster, you need a shared filesystem (NFS) for storing + * the key file otherwise different keys will be used in each instance. + *

+ * 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. + *

+ * The default algorithm for the JWT is currently {@link org.apache.archiva.redback.configuration.UserConfigurationKeys#AUTHENTICATION_JWT_SIGALG_ES384} + */ +@Service( "authenticator#jwt" ) public class JwtAuthenticator extends AbstractAuthenticator implements Authenticator { private static final Logger log = LoggerFactory.getLogger( JwtAuthenticator.class ); + public static final String DEFAULT_LIFETIME = "14400000"; + public static final String DEFAULT_KEYFILE = "jwt-key.xml"; public static final String ID = "JwtAuthenticator"; public static final String PROP_PRIV_ALG = "privateAlgorithm"; public static final String PROP_PRIV_FORMAT = "privateFormat"; @@ -71,19 +107,52 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic public static final String PROP_PUB_FORMAT = "publicFormat"; public static final String PROP_PRIVATEKEY = "privateKey"; public static final String PROP_PUBLICKEY = "publicKey"; + public static final String PROP_KEYID = "keyId"; + private static final String ISSUER = "archiva.apache.org/redback"; @Inject @Named( value = "userConfiguration#default" ) UserConfiguration userConfiguration; - boolean symmetricAlg = true; - Key key; - Key publicKey; - String sigAlg; + boolean symmetricAlgorithm = true; + boolean fileStore = false; + LinkedHashMap secretKey; + LinkedHashMap keyPair; + String signatureAlgorithm; String keystoreType; Path keystoreFilePath; + int maxInMemoryKeys = 5; + AtomicLong keyCounter; + final SigningKeyResolver resolver = new SigningKeyResolver( ); + final ReadWriteLock lock = new ReentrantReadWriteLock( ); + private JwtParser parser; + private Duration lifetime; + public class SigningKeyResolver extends SigningKeyResolverAdapter + { + + @Override + public Key resolveSigningKey( JwsHeader jwsHeader, Claims claims ) + { + Long keyId = Long.valueOf( jwsHeader.get( JwsHeader.KEY_ID ).toString() ); + Key key; + if (symmetricAlgorithm) { + key = getSecretKey( keyId ); + } else + { + KeyPair pair = getKeyPair( keyId ); + if (pair == null) { + throw new JwtException( "Key ID not found in current list. Verification failed." ); + } + key = pair.getPublic( ); + } + if (key==null) { + throw new JwtException( "Key ID not found in current list. Verification failed." ); + } + return key; + } + } @Override public String getId( ) @@ -92,62 +161,242 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic } @PostConstruct - public void init() { - this.keystoreType = userConfiguration.getString( UserConfigurationKeys.AUTHENTICATION_JWT_KEYSTORETYPE ); - this.sigAlg = userConfiguration.getString( UserConfigurationKeys.AUTHENTICATION_JWT_SIGALG ); - if ( this.sigAlg.startsWith( "HS" ) ) { - this.symmetricAlg = true; - } else { - this.symmetricAlg = false; - } - 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( ) { - 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( ) + { + @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 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic PrivateKey privateKey = KeyFactory.getInstance( algorithm ).generatePrivate( privateSpec ); PublicKey publicKey = KeyFactory.getInstance( algorithm ).generatePublic( publicSpec ); - return new KeyPair( publicKey, privateKey ); - } else { - throw new RuntimeException( "Could not load key file from " + filePath ); + return new KeyHolder( keyId, new KeyPair( publicKey, privateKey ) ); + } + else + { + throw new FileNotFoundException( "Keyfile does not exist " + filePath ); } } - private void writeSecretKey(Path filePath, SecretKey key) throws IOException + private void writeSecretKey( Path filePath, Long id, Key key ) throws IOException { log.info( "Writing secret key algorithm=" + key.getAlgorithm( ) + ", format=" + key.getFormat( ) + " to file " + filePath ); Properties props = new Properties( ); @@ -171,29 +422,39 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic { props.setProperty( PROP_PRIV_FORMAT, key.getFormat( ) ); } - props.setProperty( PROP_PRIVATEKEY, String.valueOf( Base64.getEncoder( ).encode( key.getEncoded( ) ) ) ); + props.setProperty( PROP_KEYID, id.toString() ); + props.setProperty( PROP_PRIVATEKEY, Base64.getEncoder( ).encodeToString( key.getEncoded( ) ) ); try ( OutputStream out = Files.newOutputStream( filePath ) ) { props.storeToXML( out, "Key for JWT signing" ); } try { - Files.setPosixFilePermissions( filePath, PosixFilePermissions.fromString( "600" ) ); - } catch (Exception e) { - log.error( "Could not set file permissions for " + filePath ); + Files.setPosixFilePermissions( filePath, PosixFilePermissions.fromString( "rw-------" ) ); + } + catch ( Exception e ) + { + log.error( "Could not set file permissions for {}: {}", filePath, e.getMessage( ), e ); } } - private void writeKeyPair(Path filePath, PrivateKey privateKey, PublicKey publicKey) { + private void writeKeyPair( Path filePath, Long id, KeyPair keyPair ) throws IOException + { + PrivateKey privateKey = keyPair.getPrivate( ); + PublicKey publicKey = keyPair.getPublic( ); + log.info( "Writing private key algorithm=" + privateKey.getAlgorithm( ) + ", format=" + privateKey.getFormat( ) + " to file " + filePath ); log.info( "Writing public key algorithm=" + publicKey.getAlgorithm( ) + ", format=" + publicKey.getFormat( ) + " to file " + filePath ); Properties props = new Properties( ); props.setProperty( PROP_PRIV_ALG, privateKey.getAlgorithm( ) ); - if (privateKey.getFormat()!=null) { + if ( privateKey.getFormat( ) != null ) + { props.setProperty( PROP_PRIV_FORMAT, privateKey.getFormat( ) ); } + props.setProperty( PROP_KEYID, id.toString( ) ); props.setProperty( PROP_PUB_ALG, publicKey.getAlgorithm( ) ); - if (publicKey.getFormat()!=null) { + if ( publicKey.getFormat( ) != null ) + { props.setProperty( PROP_PUB_FORMAT, publicKey.getFormat( ) ); } PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec( privateKey.getEncoded( ) ); @@ -201,23 +462,283 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic props.setProperty( PROP_PRIVATEKEY, Base64.getEncoder( ).encodeToString( privateSpec.getEncoded( ) ) ); props.setProperty( PROP_PUBLICKEY, Base64.getEncoder( ).encodeToString( publicSpec.getEncoded( ) ) ); + try ( OutputStream out = Files.newOutputStream( filePath ) ) + { + props.storeToXML( out, "Key pair for JWT signing" ); + } + try + { + Files.setPosixFilePermissions( filePath, PosixFilePermissions.fromString( "rw-------" ) ); + } + catch ( Exception e ) + { + log.error( "Could not set file permissions for {}: {}", filePath, e.getMessage( ), e ); + } } @Override public boolean supportsDataSource( AuthenticationDataSource source ) { - return (source instanceof TokenBasedAuthenticationDataSource); + return ( source instanceof TokenBasedAuthenticationDataSource ); } @Override - public AuthenticationResult authenticate( AuthenticationDataSource source ) throws AccountLockedException, AuthenticationException, MustChangePasswordException + public AuthenticationResult authenticate( AuthenticationDataSource source ) throws AuthenticationException { - if (source instanceof TokenBasedAuthenticationDataSource ) { + if ( source instanceof TokenBasedAuthenticationDataSource ) + { TokenBasedAuthenticationDataSource tSource = (TokenBasedAuthenticationDataSource) source; - return null; - } else { + String jwt = tSource.getToken( ); + AuthenticationResult result; + try + { + String subject = verify( jwt ); + result = new AuthenticationResult( true, subject, null ); + } catch (AuthenticationException e) { + result = new AuthenticationResult( false, source.getUsername(), e ); + } + return result; + } + else + { throw new AuthenticationException( "The provided authentication source is not suitable for this authenticator" ); } } + + /** + * Creates a new signing key and uses this for new tokens. It will keep {@link #maxInMemoryKeys} keys in the + * list for jwt verification. + */ + public Long renewSigningKey( ) + { + final Long id = addNewKey( ); + if (this.fileStore) + { + if ( this.symmetricAlgorithm ) + { + try + { + writeSecretKey( this.keystoreFilePath, id, getSecretKey( id ) ); + } + catch ( IOException e ) + { + log.error( "Could not write to keyfile {}: {}", this.keystoreFilePath, e.getMessage( ), e ); + } + } + else + { + try + { + writeKeyPair( this.keystoreFilePath, id, getKeyPair( id ) ); + } + catch ( IOException e ) + { + log.error( "Could not write to keyfile {}: {}", this.keystoreFilePath, e.getMessage( ), e ); + } + } + } + return id; + } + + private static class KeyHolder { + final Long id; + final SecretKey secretKey; + final KeyPair keyPair; + + KeyHolder(Long id, SecretKey key) { + this.id = id; + this.secretKey = key; + this.keyPair = null; + } + KeyHolder(Long id, KeyPair key) { + this.id = id; + this.secretKey = null; + this.keyPair = key; + } + + public Long getId( ) + { + return id; + } + + public SecretKey getSecretKey( ) + { + return secretKey; + } + + public KeyPair getKeyPair( ) + { + return keyPair; + } + + public Key getSignerKey() { + return keyPair != null ? this.keyPair.getPrivate( ) : this.secretKey; + } + } + + private KeyHolder getSignerKey() { + final Long id = keyCounter.get( ); + if (this.symmetricAlgorithm) { + return new KeyHolder( id, getSecretKey( id ) ); + } else { + return new KeyHolder( id, getKeyPair( id ) ); + } + } + + /** + * Creates a token for the given user id. The token contains the following data: + *

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 origin + * 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 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 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 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 00000000..83d3757b --- /dev/null +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/resources/META-INF/spring-context.xml @@ -0,0 +1,34 @@ + + + + + + + + + \ 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 00000000..f7b16c4d --- /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 + */ +public abstract class AbstractJwtTest +{ + protected JwtAuthenticator jwtAuthenticator; + protected DefaultUserConfiguration configuration; + protected CommonsConfigurationRegistry registry; + protected BaseConfiguration saveConfig; + + protected void init( Map 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( "\n" + + " \n" + + " \n" + + " " ); + this.registry.initialize(); + this.saveConfig = new BaseConfiguration( ); + this.registry.addConfiguration( this.saveConfig, "save", "org.apache.archiva.redback" ); + for (Map.Entry 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 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 00000000..63a0d853 --- /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 + */ +class JwtAuthenticatorFilebasedPublicKeyTest extends AbstractJwtTest +{ + + @BeforeEach + void init() throws RegistryException, UserConfigurationException + { + Map 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 00000000..ecbce686 --- /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 + */ +@TestMethodOrder( MethodOrderer.OrderAnnotation.class ) +class JwtAuthenticatorFilebasedTest extends AbstractJwtTest +{ + + @BeforeEach + void init() throws RegistryException, UserConfigurationException + { + Map 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 00000000..fa876f74 --- /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 + */ +@TestMethodOrder( MethodOrderer.OrderAnnotation.class ) +class JwtAuthenticatorMemorybasedTest extends AbstractJwtTest +{ + @BeforeEach + void init() throws RegistryException, UserConfigurationException + { + Map 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 00000000..d3c88166 --- /dev/null +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/resources/log4j2-test.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + 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 00000000..c84059f1 --- /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 548d2399..84a99fa9 100644 --- a/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/DefaultUserConfiguration.java +++ b/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/DefaultUserConfiguration.java @@ -57,7 +57,7 @@ public class DefaultUserConfiguration private Registry lookupRegistry; - private static final String PREFIX = "org.apache.archiva.redback"; + public static final String PREFIX = "org.apache.archiva.redback"; @Inject @Named(value = "commons-configuration") diff --git a/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java b/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java index 1ee7c827..2cd33412 100644 --- a/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java +++ b/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java @@ -194,6 +194,8 @@ public interface UserConfigurationKeys String AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY = "memory"; String AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE = "plainfile"; String AUTHENTICATION_JWT_SIGALG = "authentication.jwt.signatureAlgorithm"; + String AUTHENTICATION_JWT_MAX_KEYS = "authentication.jwt.maxInMemoryKeys"; + /** * HMAC using SHA-256 */ @@ -249,4 +251,9 @@ public interface UserConfigurationKeys */ String AUTHENTICATION_JWT_KEYFILE = "authentication.jwt.keyfile"; + /** + * The lifetime in ms of the generated tokens. + */ + String AUTHENTICATION_JWT_LIFETIME_MS = "authentication.jwt.lifetimeMs"; + } diff --git a/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties b/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties index 3cdd1d12..90783b10 100644 --- a/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties +++ b/redback-configuration/src/main/resources/org/apache/archiva/redback/config-defaults.properties @@ -156,4 +156,5 @@ rest.csrffilter.disableTokenValidation=false # Configuration for JWT authentication authentication.jwt.keystoreType=memory authentication.jwt.signatureAlgorithm=HS384 -authentication.jwt.keyfile=jwt-key.xml \ No newline at end of file +authentication.jwt.keyfile=jwt-key.xml +authentication.jwt.maxInMemoryKeys=5 \ No newline at end of file