Adding implementation of JWT token authentication
This commit is contained in:
parent
3ff31adf4c
commit
9ca5514bed
|
@ -20,6 +20,8 @@ package org.apache.archiva.redback.authentication;
|
|||
*/
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
|
@ -39,11 +41,10 @@ public final class SimpleTokenData implements Serializable, TokenData {
|
|||
private static final long serialVersionUID = 5907745449771921813L;
|
||||
|
||||
private final String user;
|
||||
private final Date created;
|
||||
private final Date validBefore;
|
||||
private final Instant created;
|
||||
private final Instant validBefore;
|
||||
private final long nonce;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new token info instance for the given user.
|
||||
* The lifetime in milliseconds defines the invalidation date by
|
||||
|
@ -55,8 +56,8 @@ public final class SimpleTokenData implements Serializable, TokenData {
|
|||
*/
|
||||
public SimpleTokenData(final String user, final long lifetime, final long nonce) {
|
||||
this.user=user;
|
||||
this.created=new Date();
|
||||
this.validBefore =new Date(created.getTime()+lifetime);
|
||||
this.created = Instant.now( );
|
||||
this.validBefore = created.plus( Duration.ofMillis( lifetime ) );
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
|
@ -66,12 +67,12 @@ public final class SimpleTokenData implements Serializable, TokenData {
|
|||
}
|
||||
|
||||
@Override
|
||||
public final Date created() {
|
||||
public final Instant created() {
|
||||
return created;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Date validBefore() {
|
||||
public final Instant validBefore() {
|
||||
return validBefore;
|
||||
}
|
||||
|
||||
|
@ -82,7 +83,7 @@ public final class SimpleTokenData implements Serializable, TokenData {
|
|||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return (System.currentTimeMillis())<validBefore.getTime();
|
||||
return Instant.now( ).isBefore( validBefore );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -19,18 +19,28 @@ package org.apache.archiva.redback.authentication.jwt;
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jws;
|
||||
import io.jsonwebtoken.JwsHeader;
|
||||
import io.jsonwebtoken.Jwt;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.JwtParser;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.SigningKeyResolverAdapter;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.apache.archiva.redback.authentication.AbstractAuthenticator;
|
||||
import org.apache.archiva.redback.authentication.AuthenticationDataSource;
|
||||
import org.apache.archiva.redback.authentication.AuthenticationException;
|
||||
import org.apache.archiva.redback.authentication.AuthenticationResult;
|
||||
import org.apache.archiva.redback.authentication.Authenticator;
|
||||
import org.apache.archiva.redback.authentication.SimpleTokenData;
|
||||
import org.apache.archiva.redback.authentication.StringToken;
|
||||
import org.apache.archiva.redback.authentication.Token;
|
||||
import org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource;
|
||||
import org.apache.archiva.redback.authentication.TokenData;
|
||||
import org.apache.archiva.redback.configuration.UserConfiguration;
|
||||
import org.apache.archiva.redback.configuration.UserConfigurationKeys;
|
||||
import org.apache.archiva.redback.policy.AccountLockedException;
|
||||
import org.apache.archiva.redback.policy.MustChangePasswordException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
@ -40,11 +50,13 @@ import javax.crypto.SecretKey;
|
|||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
|
@ -55,15 +67,39 @@ import java.security.PublicKey;
|
|||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
import static org.apache.archiva.redback.configuration.UserConfigurationKeys.*;
|
||||
|
||||
@Service("authenticator#jwt")
|
||||
/**
|
||||
* Authenticator for JWT tokens. This authenticator needs a secret key or keypair depending
|
||||
* on the used algorithm for signing and verification.
|
||||
* The key can be either volatile in memory, which means a new one is created, with each
|
||||
* start of the service. Or it can be stored in a file.
|
||||
* If this service is running in a cluster, you need a shared filesystem (NFS) for storing
|
||||
* the key file otherwise different keys will be used in each instance.
|
||||
* <p>
|
||||
* You can renew the used key ({@link #renewSigningKey()}). The authenticator keeps a fixed
|
||||
* sized list of the last keys used and stores the key identifier in the JWT header.
|
||||
* <p>
|
||||
* The default algorithm for the JWT is currently {@link org.apache.archiva.redback.configuration.UserConfigurationKeys#AUTHENTICATION_JWT_SIGALG_ES384}
|
||||
*/
|
||||
@Service( "authenticator#jwt" )
|
||||
public class JwtAuthenticator extends AbstractAuthenticator implements Authenticator
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger( JwtAuthenticator.class );
|
||||
|
||||
public static final String DEFAULT_LIFETIME = "14400000";
|
||||
public static final String DEFAULT_KEYFILE = "jwt-key.xml";
|
||||
public static final String ID = "JwtAuthenticator";
|
||||
public static final String PROP_PRIV_ALG = "privateAlgorithm";
|
||||
public static final String PROP_PRIV_FORMAT = "privateFormat";
|
||||
|
@ -71,19 +107,52 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
|
|||
public static final String PROP_PUB_FORMAT = "publicFormat";
|
||||
public static final String PROP_PRIVATEKEY = "privateKey";
|
||||
public static final String PROP_PUBLICKEY = "publicKey";
|
||||
public static final String PROP_KEYID = "keyId";
|
||||
private static final String ISSUER = "archiva.apache.org/redback";
|
||||
|
||||
|
||||
@Inject
|
||||
@Named( value = "userConfiguration#default" )
|
||||
UserConfiguration userConfiguration;
|
||||
|
||||
boolean symmetricAlg = true;
|
||||
Key key;
|
||||
Key publicKey;
|
||||
String sigAlg;
|
||||
boolean symmetricAlgorithm = true;
|
||||
boolean fileStore = false;
|
||||
LinkedHashMap<Long, SecretKey> secretKey;
|
||||
LinkedHashMap<Long, KeyPair> keyPair;
|
||||
String signatureAlgorithm;
|
||||
String keystoreType;
|
||||
Path keystoreFilePath;
|
||||
int maxInMemoryKeys = 5;
|
||||
AtomicLong keyCounter;
|
||||
final SigningKeyResolver resolver = new SigningKeyResolver( );
|
||||
final ReadWriteLock lock = new ReentrantReadWriteLock( );
|
||||
private JwtParser parser;
|
||||
private Duration lifetime;
|
||||
|
||||
public class SigningKeyResolver extends SigningKeyResolverAdapter
|
||||
{
|
||||
|
||||
@Override
|
||||
public Key resolveSigningKey( JwsHeader jwsHeader, Claims claims )
|
||||
{
|
||||
Long keyId = Long.valueOf( jwsHeader.get( JwsHeader.KEY_ID ).toString() );
|
||||
Key key;
|
||||
if (symmetricAlgorithm) {
|
||||
key = getSecretKey( keyId );
|
||||
} else
|
||||
{
|
||||
KeyPair pair = getKeyPair( keyId );
|
||||
if (pair == null) {
|
||||
throw new JwtException( "Key ID not found in current list. Verification failed." );
|
||||
}
|
||||
key = pair.getPublic( );
|
||||
}
|
||||
if (key==null) {
|
||||
throw new JwtException( "Key ID not found in current list. Verification failed." );
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId( )
|
||||
|
@ -92,62 +161,242 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
|
|||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.keystoreType = userConfiguration.getString( UserConfigurationKeys.AUTHENTICATION_JWT_KEYSTORETYPE );
|
||||
this.sigAlg = userConfiguration.getString( UserConfigurationKeys.AUTHENTICATION_JWT_SIGALG );
|
||||
if ( this.sigAlg.startsWith( "HS" ) ) {
|
||||
this.symmetricAlg = true;
|
||||
} else {
|
||||
this.symmetricAlg = false;
|
||||
}
|
||||
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 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
|
|||
PrivateKey privateKey = KeyFactory.getInstance( algorithm ).generatePrivate( privateSpec );
|
||||
PublicKey publicKey = KeyFactory.getInstance( algorithm ).generatePublic( publicSpec );
|
||||
|
||||
return new KeyPair( publicKey, privateKey );
|
||||
} else {
|
||||
throw new RuntimeException( "Could not load key file from " + filePath );
|
||||
return new KeyHolder( keyId, new KeyPair( publicKey, privateKey ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FileNotFoundException( "Keyfile does not exist " + filePath );
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSecretKey(Path filePath, SecretKey key) throws IOException
|
||||
private void writeSecretKey( Path filePath, Long id, Key key ) throws IOException
|
||||
{
|
||||
log.info( "Writing secret key algorithm=" + key.getAlgorithm( ) + ", format=" + key.getFormat( ) + " to file " + filePath );
|
||||
Properties props = new Properties( );
|
||||
|
@ -171,29 +422,39 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
|
|||
{
|
||||
props.setProperty( PROP_PRIV_FORMAT, key.getFormat( ) );
|
||||
}
|
||||
props.setProperty( PROP_PRIVATEKEY, String.valueOf( Base64.getEncoder( ).encode( key.getEncoded( ) ) ) );
|
||||
props.setProperty( PROP_KEYID, id.toString() );
|
||||
props.setProperty( PROP_PRIVATEKEY, Base64.getEncoder( ).encodeToString( key.getEncoded( ) ) );
|
||||
try ( OutputStream out = Files.newOutputStream( filePath ) )
|
||||
{
|
||||
props.storeToXML( out, "Key for JWT signing" );
|
||||
}
|
||||
try
|
||||
{
|
||||
Files.setPosixFilePermissions( filePath, PosixFilePermissions.fromString( "600" ) );
|
||||
} catch (Exception e) {
|
||||
log.error( "Could not set file permissions for " + filePath );
|
||||
Files.setPosixFilePermissions( filePath, PosixFilePermissions.fromString( "rw-------" ) );
|
||||
}
|
||||
catch ( Exception e )
|
||||
{
|
||||
log.error( "Could not set file permissions for {}: {}", filePath, e.getMessage( ), e );
|
||||
}
|
||||
}
|
||||
|
||||
private void writeKeyPair(Path filePath, PrivateKey privateKey, PublicKey publicKey) {
|
||||
private void writeKeyPair( Path filePath, Long id, KeyPair keyPair ) throws IOException
|
||||
{
|
||||
PrivateKey privateKey = keyPair.getPrivate( );
|
||||
PublicKey publicKey = keyPair.getPublic( );
|
||||
|
||||
log.info( "Writing private key algorithm=" + privateKey.getAlgorithm( ) + ", format=" + privateKey.getFormat( ) + " to file " + filePath );
|
||||
log.info( "Writing public key algorithm=" + publicKey.getAlgorithm( ) + ", format=" + publicKey.getFormat( ) + " to file " + filePath );
|
||||
Properties props = new Properties( );
|
||||
props.setProperty( PROP_PRIV_ALG, privateKey.getAlgorithm( ) );
|
||||
if (privateKey.getFormat()!=null) {
|
||||
if ( privateKey.getFormat( ) != null )
|
||||
{
|
||||
props.setProperty( PROP_PRIV_FORMAT, privateKey.getFormat( ) );
|
||||
}
|
||||
props.setProperty( PROP_KEYID, id.toString( ) );
|
||||
props.setProperty( PROP_PUB_ALG, publicKey.getAlgorithm( ) );
|
||||
if (publicKey.getFormat()!=null) {
|
||||
if ( publicKey.getFormat( ) != null )
|
||||
{
|
||||
props.setProperty( PROP_PUB_FORMAT, publicKey.getFormat( ) );
|
||||
}
|
||||
PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec( privateKey.getEncoded( ) );
|
||||
|
@ -201,23 +462,283 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
|
|||
props.setProperty( PROP_PRIVATEKEY, Base64.getEncoder( ).encodeToString( privateSpec.getEncoded( ) ) );
|
||||
props.setProperty( PROP_PUBLICKEY, Base64.getEncoder( ).encodeToString( publicSpec.getEncoded( ) ) );
|
||||
|
||||
try ( OutputStream out = Files.newOutputStream( filePath ) )
|
||||
{
|
||||
props.storeToXML( out, "Key pair for JWT signing" );
|
||||
}
|
||||
try
|
||||
{
|
||||
Files.setPosixFilePermissions( filePath, PosixFilePermissions.fromString( "rw-------" ) );
|
||||
}
|
||||
catch ( Exception e )
|
||||
{
|
||||
log.error( "Could not set file permissions for {}: {}", filePath, e.getMessage( ), e );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsDataSource( AuthenticationDataSource source )
|
||||
{
|
||||
return (source instanceof TokenBasedAuthenticationDataSource);
|
||||
return ( source instanceof TokenBasedAuthenticationDataSource );
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationResult authenticate( AuthenticationDataSource source ) throws AccountLockedException, AuthenticationException, MustChangePasswordException
|
||||
public AuthenticationResult authenticate( AuthenticationDataSource source ) throws AuthenticationException
|
||||
{
|
||||
if (source instanceof TokenBasedAuthenticationDataSource ) {
|
||||
if ( source instanceof TokenBasedAuthenticationDataSource )
|
||||
{
|
||||
TokenBasedAuthenticationDataSource tSource = (TokenBasedAuthenticationDataSource) source;
|
||||
return null;
|
||||
} else {
|
||||
String jwt = tSource.getToken( );
|
||||
AuthenticationResult result;
|
||||
try
|
||||
{
|
||||
String subject = verify( jwt );
|
||||
result = new AuthenticationResult( true, subject, null );
|
||||
} catch (AuthenticationException e) {
|
||||
result = new AuthenticationResult( false, source.getUsername(), e );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new AuthenticationException( "The provided authentication source is not suitable for this authenticator" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new signing key and uses this for new tokens. It will keep {@link #maxInMemoryKeys} keys in the
|
||||
* list for jwt verification.
|
||||
*/
|
||||
public Long renewSigningKey( )
|
||||
{
|
||||
final Long id = addNewKey( );
|
||||
if (this.fileStore)
|
||||
{
|
||||
if ( this.symmetricAlgorithm )
|
||||
{
|
||||
try
|
||||
{
|
||||
writeSecretKey( this.keystoreFilePath, id, getSecretKey( id ) );
|
||||
}
|
||||
catch ( IOException e )
|
||||
{
|
||||
log.error( "Could not write to keyfile {}: {}", this.keystoreFilePath, e.getMessage( ), e );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
writeKeyPair( this.keystoreFilePath, id, getKeyPair( id ) );
|
||||
}
|
||||
catch ( IOException e )
|
||||
{
|
||||
log.error( "Could not write to keyfile {}: {}", this.keystoreFilePath, e.getMessage( ), e );
|
||||
}
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private static class KeyHolder {
|
||||
final Long id;
|
||||
final SecretKey secretKey;
|
||||
final KeyPair keyPair;
|
||||
|
||||
KeyHolder(Long id, SecretKey key) {
|
||||
this.id = id;
|
||||
this.secretKey = key;
|
||||
this.keyPair = null;
|
||||
}
|
||||
KeyHolder(Long id, KeyPair key) {
|
||||
this.id = id;
|
||||
this.secretKey = null;
|
||||
this.keyPair = key;
|
||||
}
|
||||
|
||||
public Long getId( )
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public SecretKey getSecretKey( )
|
||||
{
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
public KeyPair getKeyPair( )
|
||||
{
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
public Key getSignerKey() {
|
||||
return keyPair != null ? this.keyPair.getPrivate( ) : this.secretKey;
|
||||
}
|
||||
}
|
||||
|
||||
private KeyHolder getSignerKey() {
|
||||
final Long id = keyCounter.get( );
|
||||
if (this.symmetricAlgorithm) {
|
||||
return new KeyHolder( id, getSecretKey( id ) );
|
||||
} else {
|
||||
return new KeyHolder( id, getKeyPair( id ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a token for the given user id. The token contains the following data:
|
||||
* <ul>
|
||||
* <li>the userid as subject</li>
|
||||
* <li>a issuer archiva.apache.org/redback</li>
|
||||
* <li>a id header with the key id</li>
|
||||
* </ul>the user id as subject.
|
||||
*
|
||||
* @param userId the user identifier to set as subject
|
||||
* @return the token string
|
||||
*/
|
||||
public Token generateToken( String userId )
|
||||
{
|
||||
final KeyHolder signerKey = getSignerKey( );
|
||||
Instant now = Instant.now( );
|
||||
Instant expiration = now.plus( lifetime );
|
||||
final String token = Jwts.builder( )
|
||||
.setSubject( userId )
|
||||
.setIssuer( ISSUER )
|
||||
.setIssuedAt( Date.from( now ) )
|
||||
.setExpiration( Date.from( expiration ) )
|
||||
.setHeaderParam( JwsHeader.KEY_ID, signerKey.getId( ).toString( ) )
|
||||
.signWith( signerKey.getSignerKey( ) ).compact( );
|
||||
TokenData metadata = new SimpleTokenData( userId, lifetime.toMillis( ), 0 );
|
||||
return new StringToken( token, metadata );
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to renew a token based on the origin token. If the presented <code>origin</code>
|
||||
* is valid, a new token with refreshed expiration time will be returned.
|
||||
*
|
||||
* @param origin the origin token
|
||||
* @return the newly created token
|
||||
* @throws AuthenticationException if the given origin token is not valid
|
||||
*/
|
||||
public Token renewToken(String origin) throws AuthenticationException {
|
||||
try
|
||||
{
|
||||
Jws<Claims> signature = this.parser.parseClaimsJws( origin );
|
||||
return generateToken( signature.getBody( ).getSubject( ) );
|
||||
} catch (JwtException e) {
|
||||
throw new AuthenticationException( "Could not renew the token " + e.getMessage( ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given token and returns the JWS metadata stored in the token.
|
||||
*
|
||||
* @param token the token string
|
||||
* @return the parsed data
|
||||
* @throws JwtException if the token data is not valid anymore
|
||||
*/
|
||||
public Jws<Claims> parseToken( String token) throws JwtException {
|
||||
return parser.parseClaimsJws( token );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the given JWT Token and returns the stored subject, if successful
|
||||
* If the verification failed a AuthenticationException is thrown.
|
||||
* @param token the JWT representation
|
||||
* @return the subject of the JWT
|
||||
* @throws AuthenticationException if the verification failed
|
||||
*/
|
||||
public String verify( String token ) throws AuthenticationException
|
||||
{
|
||||
try
|
||||
{
|
||||
Jws<Claims> signature = this.parser.parseClaimsJws( token );
|
||||
String subject = signature.getBody( ).getSubject( );
|
||||
if ( StringUtils.isEmpty( subject ) )
|
||||
{
|
||||
throw new AuthenticationException( "Subject in JWT is empty" );
|
||||
}
|
||||
return subject;
|
||||
}
|
||||
catch ( JwtException e )
|
||||
{
|
||||
throw new AuthenticationException( e.getMessage( ), e );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all signing keys and creates a new one.
|
||||
*/
|
||||
public void revokeSigningKeys() {
|
||||
lock.writeLock( ).lock( );
|
||||
try {
|
||||
this.secretKey.clear();
|
||||
this.keyPair.clear();
|
||||
renewSigningKey( );
|
||||
} finally
|
||||
{
|
||||
lock.writeLock( ).unlock( );
|
||||
}
|
||||
}
|
||||
|
||||
private SigningKeyResolver getResolver( )
|
||||
{
|
||||
return this.resolver;
|
||||
}
|
||||
|
||||
public boolean usesSymmetricAlgorithm( )
|
||||
{
|
||||
return symmetricAlgorithm;
|
||||
}
|
||||
|
||||
public String getSignatureAlgorithm( )
|
||||
{
|
||||
return signatureAlgorithm;
|
||||
}
|
||||
|
||||
public String getKeystoreType( )
|
||||
{
|
||||
return keystoreType;
|
||||
}
|
||||
|
||||
public Path getKeystoreFilePath( )
|
||||
{
|
||||
return keystoreFilePath;
|
||||
}
|
||||
|
||||
public int getMaxInMemoryKeys( )
|
||||
{
|
||||
return maxInMemoryKeys;
|
||||
}
|
||||
|
||||
public int getCurrentKeyListSize() {
|
||||
if (symmetricAlgorithm) {
|
||||
return secretKey.size( );
|
||||
} else {
|
||||
return keyPair.size( );
|
||||
}
|
||||
}
|
||||
|
||||
public Long getCurrentKeyId() {
|
||||
return keyCounter.get( );
|
||||
}
|
||||
|
||||
public Duration getTokenLifetime() {
|
||||
return this.lifetime;
|
||||
}
|
||||
|
||||
public void setTokenLifetime(Duration lifetime) {
|
||||
this.lifetime = lifetime;
|
||||
}
|
||||
|
||||
public UserConfiguration getUserConfiguration( )
|
||||
{
|
||||
return userConfiguration;
|
||||
}
|
||||
|
||||
public void setUserConfiguration( UserConfiguration userConfiguration )
|
||||
{
|
||||
this.userConfiguration = userConfiguration;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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( ) );
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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() );
|
||||
}
|
||||
|
||||
}
|
|
@ -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() );
|
||||
}
|
||||
|
||||
}
|
|
@ -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( ) );
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
|
@ -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
|
|
@ -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")
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
authentication.jwt.keyfile=jwt-key.xml
|
||||
authentication.jwt.maxInMemoryKeys=5
|
Loading…
Reference in New Issue