Adding refresh token for authentication

This commit is contained in:
Martin Stockhammer 2020-07-12 21:03:37 +02:00
parent f88a365bc8
commit 15ad042ac1
24 changed files with 1111 additions and 157 deletions

View File

@ -61,6 +61,22 @@ public final class SimpleTokenData implements Serializable, TokenData {
this.nonce = nonce; this.nonce = nonce;
} }
/**
* Creates a new token info instance for the given user.
* The lifetime in milliseconds defines the invalidation date by
* adding the lifetime to the current time of instantiation.
*
* @param user The user name
* @param lifetime The number of milliseconds after that the token is invalid
* @param nonce Should be a random number and different for each instance.
*/
public SimpleTokenData(final String user, final Duration lifetime, final long nonce) {
this.user=user;
this.created = Instant.now( );
this.validBefore = created.plus( lifetime );
this.nonce = nonce;
}
@Override @Override
public final String getUser() { public final String getUser() {
return user; return user;

View File

@ -26,23 +26,34 @@ package org.apache.archiva.redback.authentication;
public class StringToken implements Token public class StringToken implements Token
{ {
final TokenData metadata; final TokenData metadata;
final String token; final String data;
final String id;
final TokenType type;
public StringToken(String tokenData, TokenData metadata) { public StringToken(String id, String tokenData, TokenData metadata) {
this.token = tokenData; this.id = id;
this.data = tokenData;
this.metadata = metadata; this.metadata = metadata;
this.type = TokenType.ACCESS_TOKEN;
}
public StringToken(TokenType type, String id, String tokenData, TokenData metadata) {
this.id = id;
this.data = tokenData;
this.metadata = metadata;
this.type = type;
} }
@Override @Override
public String getData( ) public String getData( )
{ {
return token; return data;
} }
@Override @Override
public byte[] getBytes( ) public byte[] getBytes( )
{ {
return token.getBytes( ); return data.getBytes( );
} }
@Override @Override
@ -50,4 +61,16 @@ public class StringToken implements Token
{ {
return metadata; return metadata;
} }
@Override
public String getId( )
{
return id;
}
@Override
public TokenType getType( )
{
return type;
}
} }

View File

@ -26,9 +26,34 @@ package org.apache.archiva.redback.authentication;
public interface Token public interface Token
{ {
/**
* The token id, if it exists, otherwise a empty string.
* @return
*/
String getId();
/**
* Returns the token type (access or refresh token)
* @return the token type
*/
TokenType getType();
/**
* The string representation of the token data. It depends on the token algorithm,
* what kind of string conversion is used (e.g. Base64)
* @return the token string
*/
String getData(); String getData();
/**
* The token as byte array
* @return
*/
byte[] getBytes(); byte[] getBytes();
/**
* The token meta data, like expiration time.
* @return the metadata
*/
TokenData getMetadata(); TokenData getMetadata();
} }

View File

@ -0,0 +1,48 @@
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.
*/
/**
* @author Martin Stockhammer <martin_s@apache.org>
*/
public enum TokenType
{
REFRESH_TOKEN("refresh_token"), ACCESS_TOKEN( "access_token" ), ALL( "*" ),;
private String claim;
TokenType( String claim )
{
this.claim = claim;
}
public String getClaim() {
return this.claim;
}
public static TokenType ofClaim(String claim) {
TokenType[] vals = values( );
for (int i=0; i< vals.length; i++) {
if (vals[i].getClaim().equals(claim)) {
return vals[i];
}
}
return null;
}
}

View File

@ -21,12 +21,14 @@ package org.apache.archiva.redback.authentication.jwt;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.IncorrectClaimException;
import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.MissingClaimException;
import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SigningKeyResolverAdapter; import io.jsonwebtoken.SigningKeyResolverAdapter;
import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.UnsupportedJwtException;
@ -44,6 +46,7 @@ import org.apache.archiva.redback.authentication.StringToken;
import org.apache.archiva.redback.authentication.Token; import org.apache.archiva.redback.authentication.Token;
import org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource; import org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource;
import org.apache.archiva.redback.authentication.TokenData; import org.apache.archiva.redback.authentication.TokenData;
import org.apache.archiva.redback.authentication.TokenType;
import org.apache.archiva.redback.configuration.UserConfiguration; import org.apache.archiva.redback.configuration.UserConfiguration;
import org.apache.archiva.redback.configuration.UserConfigurationKeys; import org.apache.archiva.redback.configuration.UserConfigurationKeys;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -78,9 +81,11 @@ import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
@ -140,7 +145,10 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
{ {
private static final Logger log = LoggerFactory.getLogger( JwtAuthenticator.class ); private static final Logger log = LoggerFactory.getLogger( JwtAuthenticator.class );
// 4 hours for standard tokens
public static final String DEFAULT_LIFETIME = "14400000"; public static final String DEFAULT_LIFETIME = "14400000";
// 7 days for refresh tokens
public static final String DEFAULT_REFRESH_LIFETIME = "604800000";
public static final String DEFAULT_KEYFILE = "jwt-key.xml"; public static final String DEFAULT_KEYFILE = "jwt-key.xml";
public static final String ID = "JwtAuthenticator"; public static final String ID = "JwtAuthenticator";
public static final String PROP_PRIV_ALG = "privateAlgorithm"; public static final String PROP_PRIV_ALG = "privateAlgorithm";
@ -151,6 +159,7 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
public static final String PROP_PUBLICKEY = "publicKey"; public static final String PROP_PUBLICKEY = "publicKey";
public static final String PROP_KEYID = "keyId"; public static final String PROP_KEYID = "keyId";
private static final String ISSUER = "archiva.apache.org/redback"; private static final String ISSUER = "archiva.apache.org/redback";
private static final String TOKEN_TYPE = "token_type";
@Inject @Inject
@ -168,9 +177,14 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
AtomicLong keyCounter; AtomicLong keyCounter;
final SigningKeyResolver resolver = new SigningKeyResolver( ); final SigningKeyResolver resolver = new SigningKeyResolver( );
final ReadWriteLock lock = new ReentrantReadWriteLock( ); final ReadWriteLock lock = new ReentrantReadWriteLock( );
private JwtParser parser; private Duration tokenLifetime;
private Duration lifetime; private Duration refreshTokenLifetime;
private Map<TokenType, JwtParser> parserMap = new HashMap<>( );
private JwtParser getParser(TokenType type) {
return parserMap.get( type );
}
public class SigningKeyResolver extends SigningKeyResolverAdapter public class SigningKeyResolver extends SigningKeyResolverAdapter
{ {
@ -242,12 +256,24 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
// In memory key store is the default // In memory key store is the default
addNewKey( ); addNewKey( );
} }
this.parser = Jwts.parserBuilder( ) this.parserMap.put(TokenType.ALL, Jwts.parserBuilder( )
.setSigningKeyResolver( getResolver( ) ) .setSigningKeyResolver( getResolver( ) )
.requireIssuer( ISSUER ) .requireIssuer( ISSUER )
.build( ); .build( ));
this.parserMap.put(TokenType.ACCESS_TOKEN, Jwts.parserBuilder( )
.setSigningKeyResolver( getResolver( ) )
.requireIssuer( ISSUER )
.require( TOKEN_TYPE, TokenType.ACCESS_TOKEN.getClaim() )
.build( ));
this.parserMap.put(TokenType.REFRESH_TOKEN, Jwts.parserBuilder( )
.setSigningKeyResolver( getResolver( ) )
.requireIssuer( ISSUER )
.require( TOKEN_TYPE, TokenType.REFRESH_TOKEN.getClaim() )
.build( ));
lifetime = Duration.ofMillis( Long.parseLong( userConfiguration.getString( AUTHENTICATION_JWT_LIFETIME_MS, DEFAULT_LIFETIME ) ) );
tokenLifetime = Duration.ofMillis( Long.parseLong( userConfiguration.getString( AUTHENTICATION_JWT_LIFETIME_MS, DEFAULT_LIFETIME ) ) );
refreshTokenLifetime = Duration.ofMillis( Long.parseLong( userConfiguration.getString( AUTHENTICATION_JWT_REFRESH_LIFETIME_MS, DEFAULT_REFRESH_LIFETIME ) ) );
} }
private void addNewSecretKey( Long id, SecretKey key ) private void addNewSecretKey( Long id, SecretKey key )
@ -661,33 +687,94 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
{ {
final KeyHolder signerKey = getSignerKey( ); final KeyHolder signerKey = getSignerKey( );
Instant now = Instant.now( ); Instant now = Instant.now( );
Instant expiration = now.plus( lifetime ); Instant expiration = now.plus( tokenLifetime );
final String token = Jwts.builder( ) final String token = Jwts.builder( )
.setSubject( userId ) .setSubject( userId )
.setIssuer( ISSUER ) .setIssuer( ISSUER )
.claim( TOKEN_TYPE, TokenType.ACCESS_TOKEN.getClaim( ) )
.setIssuedAt( Date.from( now ) ) .setIssuedAt( Date.from( now ) )
.setExpiration( Date.from( expiration ) ) .setExpiration( Date.from( expiration ) )
.setHeaderParam( JwsHeader.KEY_ID, signerKey.getId( ).toString( ) ) .setHeaderParam( JwsHeader.KEY_ID, signerKey.getId( ).toString( ) )
.signWith( signerKey.getSignerKey( ) ).compact( ); .signWith( signerKey.getSignerKey( ) ).compact( );
TokenData metadata = new SimpleTokenData( userId, lifetime.toMillis( ), 0 ); TokenData metadata = new SimpleTokenData( userId, tokenLifetime, 0 );
return new StringToken( token, metadata ); return new StringToken("", token, metadata );
}
/**
* 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
* @param type the token type that indicates if this token is a access or refresh token
* @return the token string
*/
public Token generateToken( String userId, TokenType type )
{
if (type==TokenType.ACCESS_TOKEN) {
return generateToken( userId );
} else if (type == TokenType.REFRESH_TOKEN)
{
return generateRefreshToken( userId );
} else {
throw new RuntimeException( "Invalid token type requested" );
}
}
private Token generateRefreshToken(String userId) {
final KeyHolder signerKey = getSignerKey( );
Instant now = Instant.now( );
Instant expiration = now.plus( refreshTokenLifetime );
final String id = UUID.randomUUID( ).toString( );
final String token = Jwts.builder( )
.setSubject( userId )
.setIssuer( ISSUER )
.setIssuedAt( Date.from( now ) )
.setId( id )
.claim( TOKEN_TYPE, TokenType.REFRESH_TOKEN.getClaim() )
.setExpiration( Date.from( expiration ) )
.setHeaderParam( JwsHeader.KEY_ID, signerKey.getId( ).toString( ) )
.signWith( signerKey.getSignerKey( ) ).compact( );
TokenData metadata = new SimpleTokenData( userId, refreshTokenLifetime, 0 );
return new StringToken( TokenType.REFRESH_TOKEN, id, token, metadata );
}
/**
* Returns a token object from the given token String
*
* @param tokenData the string representation of the token
* @return the token instance
*/
public Token tokenFromString(String tokenData) {
Jws<Claims> parsedToken = parseToken( tokenData );
String userId = parsedToken.getBody( ).getSubject( );
TokenType type = TokenType.ofClaim( parsedToken.getBody( ).get( TOKEN_TYPE, String.class ) );
String id = parsedToken.getBody( ).getId( );
Instant expiration = parsedToken.getBody( ).getExpiration( ).toInstant( );
Instant issuedAt = parsedToken.getBody( ).getIssuedAt( ).toInstant( );
long lifetime = Duration.between( issuedAt, expiration ).toMillis( );
TokenData metadata = new SimpleTokenData( userId, lifetime, 0 );
return new StringToken( type, id, tokenData, metadata );
} }
/** /**
* Allows to renew a token based on the origin token. If the presented <code>origin</code> * 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. * is valid, a new token with refreshed expiration time will be returned.
* *
* @param origin the origin token * @param refreshToken the refresh token
* @return the newly created token * @return the newly created token
* @throws AuthenticationException if the given origin token is not valid * @throws AuthenticationException if the given origin token is not valid
*/ */
public Token renewToken(String origin) throws AuthenticationException { public Token refreshAccessToken( String refreshToken) throws TokenAuthenticationException {
try try
{ {
Jws<Claims> signature = this.parser.parseClaimsJws( origin ); String subject = verify( refreshToken, TokenType.REFRESH_TOKEN );
return generateToken( signature.getBody( ).getSubject( ) ); return generateToken( subject );
} catch ( JwtException e) { } catch ( JwtException e) {
throw new AuthenticationException( "Could not renew the token " + e.getMessage( ) ); throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "unknown error " + e.getMessage( ) );
} }
} }
@ -699,21 +786,26 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
* @throws JwtException if the token data is not valid anymore * @throws JwtException if the token data is not valid anymore
*/ */
public Jws<Claims> parseToken( String token) throws JwtException { public Jws<Claims> parseToken( String token) throws JwtException {
return parser.parseClaimsJws( token ); return getParser(TokenType.ALL).parseClaimsJws( token );
} }
/** /**
* Verifies the given JWT Token and returns the stored subject, if successful * Verifies the given JWT Token and returns the stored subject, if successful
* If the verification failed a AuthenticationException is thrown. * If the verification failed a TokenAuthenticationException is thrown.
* @param token the JWT representation * @param token the JWT representation
* @return the subject of the JWT * @return the subject of the JWT
* @throws AuthenticationException if the verification failed * @throws TokenAuthenticationException if the verification failed
*/ */
public String verify( String token ) throws TokenAuthenticationException public String verify( String token ) throws TokenAuthenticationException
{
return verify( token, TokenType.ACCESS_TOKEN );
}
public String verify( String token, TokenType type ) throws TokenAuthenticationException
{ {
try try
{ {
Jws<Claims> signature = this.parser.parseClaimsJws( token ); Jws<Claims> signature = getParser(type).parseClaimsJws( token );
String subject = signature.getBody( ).getSubject( ); String subject = signature.getBody( ).getSubject( );
if ( StringUtils.isEmpty( subject ) ) if ( StringUtils.isEmpty( subject ) )
{ {
@ -738,6 +830,9 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
catch (JwtKeyIdNotFoundException e) { catch (JwtKeyIdNotFoundException e) {
throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "signer key does not exist" ); throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "signer key does not exist" );
} }
catch ( MissingClaimException |IncorrectClaimException e ) {
throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "the token type is not correct - expected claim "+type.getClaim() );
}
catch ( JwtException e) { catch ( JwtException e) {
log.debug( "Unknown JwtException {}, {}", e.getClass( ), e.getMessage( ) ); log.debug( "Unknown JwtException {}, {}", e.getClass( ), e.getMessage( ) );
throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "unknown error " + e.getMessage( ) ); throw new TokenAuthenticationException( BearerError.INVALID_TOKEN, "unknown error " + e.getMessage( ) );
@ -836,7 +931,7 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
* @return the lifetime as duration * @return the lifetime as duration
*/ */
public Duration getTokenLifetime() { public Duration getTokenLifetime() {
return this.lifetime; return this.tokenLifetime;
} }
/** /**
@ -844,7 +939,7 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic
* @param lifetime the lifetime as duration * @param lifetime the lifetime as duration
*/ */
public void setTokenLifetime(Duration lifetime) { public void setTokenLifetime(Duration lifetime) {
this.lifetime = lifetime; this.tokenLifetime = lifetime;
} }
public UserConfiguration getUserConfiguration( ) public UserConfiguration getUserConfiguration( )

View File

@ -29,6 +29,7 @@ import org.apache.archiva.redback.authentication.BearerTokenAuthenticationDataSo
import org.apache.archiva.redback.authentication.PasswordBasedAuthenticationDataSource; import org.apache.archiva.redback.authentication.PasswordBasedAuthenticationDataSource;
import org.apache.archiva.redback.authentication.Token; import org.apache.archiva.redback.authentication.Token;
import org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource; import org.apache.archiva.redback.authentication.TokenBasedAuthenticationDataSource;
import org.apache.archiva.redback.authentication.TokenType;
import org.apache.archiva.redback.configuration.DefaultUserConfiguration; import org.apache.archiva.redback.configuration.DefaultUserConfiguration;
import org.apache.archiva.redback.configuration.UserConfigurationException; import org.apache.archiva.redback.configuration.UserConfigurationException;
import org.apache.commons.configuration2.BaseConfiguration; import org.apache.commons.configuration2.BaseConfiguration;
@ -39,6 +40,7 @@ import org.junit.jupiter.api.Test;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@ -119,12 +121,6 @@ public abstract class AbstractJwtTest
assertTrue( Instant.now( ).isBefore( token.getMetadata( ).validBefore( ) ) ); assertTrue( Instant.now( ).isBefore( token.getMetadata( ).validBefore( ) ) );
} }
@Test
void authenticate( )
{
}
@Test @Test
void renewSigningKey( ) void renewSigningKey( )
{ {
@ -231,5 +227,32 @@ public abstract class AbstractJwtTest
assertFalse( result.isAuthenticated( ) ); assertFalse( result.isAuthenticated( ) );
} }
@Test
void refreshToken() throws TokenAuthenticationException
{
Token token = jwtAuthenticator.generateToken( "bilbo_baggins" , TokenType.REFRESH_TOKEN);
assertNotNull( token );
assertTrue( token.getType( ).equals( TokenType.REFRESH_TOKEN ) );
UUID tokenId = UUID.fromString( token.getId( ) );
assertNotNull( tokenId );
Token accessToken = jwtAuthenticator.refreshAccessToken( token.getData() );
assertNotNull( accessToken );
assertTrue( accessToken.getType( ).equals( TokenType.ACCESS_TOKEN ) );
}
@Test
void invalidRefreshWithAccessToken() throws TokenAuthenticationException
{
Token token = jwtAuthenticator.generateToken( "bilbo_baggins");
assertNotNull( token );
assertTrue( token.getType( ).equals( TokenType.ACCESS_TOKEN ) );
TokenAuthenticationException thrownException = assertThrows( TokenAuthenticationException.class, ( ) -> {
jwtAuthenticator.refreshAccessToken( token.getData( ) );
} );
assertEquals( BearerError.INVALID_TOKEN, thrownException.getError( ) );
assertTrue( thrownException.getMessage( ).contains( "the token type is not correct - expected claim " + TokenType.REFRESH_TOKEN.getClaim( ) ) );
}
} }

View File

@ -187,25 +187,25 @@ public interface UserConfigurationKeys
String MAIL_DEFAULT_LOCALE = "mail.locale"; String MAIL_DEFAULT_LOCALE = "mail.locale";
/** /**
* Defines, where the key for JWT encryption / decryption is stored. * The property for defining, where the key for JWT encryption / decryption is stored.
* Currently only memory and plainfile are supported * Currently only memory and plainfile are supported
* {@value} * {@value}
*/ */
String AUTHENTICATION_JWT_KEYSTORETYPE = "authentication.jwt.keystoreType"; String AUTHENTICATION_JWT_KEYSTORETYPE = "authentication.jwt.keystoreType";
/** /**
* Keystore type name for memory keystore: {@value} * The property value for memory keystore: {@value}
*/ */
String AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY = "memory"; String AUTHENTICATION_JWT_KEYSTORETYPE_MEMORY = "memory";
/** /**
* Keystore type name for plain file keystore: {@value} * The property value for plain file keystore: {@value}
*/ */
String AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE = "plainfile"; String AUTHENTICATION_JWT_KEYSTORETYPE_PLAINFILE = "plainfile";
/** /**
* Defines the used signature algorithm for JWT signing: {@value} * The property for defining the used signature algorithm for JWT signing: {@value}
*/ */
String AUTHENTICATION_JWT_SIGALG = "authentication.jwt.signatureAlgorithm"; String AUTHENTICATION_JWT_SIGALG = "authentication.jwt.signatureAlgorithm";
/** /**
* Defines the maximum number of keys to keep in memory for verificatio: {@value} * The property for defining the maximum number of keys to keep in memory for verification: {@value}
*/ */
String AUTHENTICATION_JWT_MAX_KEYS = "authentication.jwt.maxInMemoryKeys"; String AUTHENTICATION_JWT_MAX_KEYS = "authentication.jwt.maxInMemoryKeys";
@ -260,13 +260,18 @@ public interface UserConfigurationKeys
/** /**
* Path to the file where the JWT key is stored: {@value} * The property for the path to the file where the JWT key is stored: {@value}
*/ */
String AUTHENTICATION_JWT_KEYFILE = "authentication.jwt.keyfile"; String AUTHENTICATION_JWT_KEYFILE = "authentication.jwt.keyfile";
/** /**
* The lifetime in ms of the generated tokens: {@value} * The property for lifetime in ms of the generated tokens: {@value}
*/ */
String AUTHENTICATION_JWT_LIFETIME_MS = "authentication.jwt.lifetimeMs"; String AUTHENTICATION_JWT_LIFETIME_MS = "authentication.jwt.lifetimeMs";
/**
* The property for lifetime in ms of the generated refresh tokens: {@value}
*/
String AUTHENTICATION_JWT_REFRESH_LIFETIME_MS = "authentication.jwt.refreshLifetimeMs";
} }

View File

@ -19,6 +19,7 @@ package org.apache.archiva.redback.rest.api.model;
*/ */
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import java.time.OffsetDateTime;
/** /**
* @author Martin Stockhammer <martin_s@apache.org> * @author Martin Stockhammer <martin_s@apache.org>
@ -27,13 +28,15 @@ import javax.xml.bind.annotation.XmlRootElement;
public class PingResult public class PingResult
{ {
boolean success; boolean success;
OffsetDateTime requestTime;
public PingResult() { public PingResult() {
this.requestTime = OffsetDateTime.now( );
} }
public PingResult( boolean success ) { public PingResult( boolean success ) {
this.success = success; this.success = success;
this.requestTime = OffsetDateTime.now( );
} }
public boolean isSuccess( ) public boolean isSuccess( )
@ -45,4 +48,14 @@ public class PingResult
{ {
this.success = success; this.success = success;
} }
public OffsetDateTime getRequestTime( )
{
return requestTime;
}
public void setRequestTime( OffsetDateTime requestTime )
{
this.requestTime = requestTime;
}
} }

View File

@ -0,0 +1,77 @@
package org.apache.archiva.redback.rest.api.model;
/*
* 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 javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* @author Martin Stockhammer <martin_s@apache.org>
*/
@XmlRootElement(name="refreshToken")
public class RefreshTokenRequest
{
String grantType;
String refreshToken;
String scope;
public RefreshTokenRequest( )
{
}
public RefreshTokenRequest( String grantType, String refreshToken, String scope )
{
this.grantType = grantType;
this.refreshToken = refreshToken;
this.scope = scope;
}
@XmlElement(name = "grant_type")
public String getGrantType( )
{
return grantType;
}
public void setGrantType( String grantType )
{
this.grantType = grantType;
}
@XmlElement(name="refresh_token")
public String getRefreshToken( )
{
return refreshToken;
}
public void setRefreshToken( String refreshToken )
{
this.refreshToken = refreshToken;
}
@XmlElement(name="scope")
public String getScope( )
{
return scope;
}
public void setScope( String scope )
{
this.scope = scope;
}
}

View File

@ -0,0 +1,156 @@
package org.apache.archiva.redback.rest.api.model;
/*
* 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 javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* @author Martin Stockhammer <martin_s@apache.org>
*/
@XmlRootElement(name="refreshToken")
public class RequestTokenRequest
{
String grantType = "authorization_code";
String clientId;
String clientSecret;
String code;
String scope = "";
String state = "";
String userId;
String password;
String redirectUri;
public RequestTokenRequest() {
}
public RequestTokenRequest( String userId, String password )
{
this.userId = userId;
this.password = password;
}
public RequestTokenRequest( String userId, String password, String scope )
{
this.userId = userId;
this.password = password;
this.scope = scope;
}
@XmlElement(name = "grant_type", required = true, nillable = false)
public String getGrantType( )
{
return grantType;
}
public void setGrantType( String grantType )
{
this.grantType = grantType;
}
@XmlElement(name="client_id", required = false, nillable = true)
public String getClientId( )
{
return clientId;
}
public void setClientId( String clientId )
{
this.clientId = clientId;
}
@XmlElement(name="client_secret", required = false, nillable = true)
public String getClientSecret( )
{
return clientSecret;
}
public void setClientSecret( String clientSecret )
{
this.clientSecret = clientSecret;
}
@XmlElement(name="scope", required = false, nillable = true)
public String getScope( )
{
return scope;
}
public void setScope( String scope )
{
this.scope = scope;
}
@XmlElement(name="user_id", required = true, nillable = false)
public String getUserId( )
{
return userId;
}
@XmlElement(name="user_id", required = true, nillable = false)
public void setUserId( String userId )
{
this.userId = userId;
}
@XmlElement(name="password", required = true, nillable = false)
public String getPassword( )
{
return password;
}
public void setPassword( String password )
{
this.password = password;
}
@XmlElement(name="code", required = false, nillable = false)
public String getCode( )
{
return code;
}
public void setCode( String code )
{
this.code = code;
}
@XmlElement(name="redirect_uri", required = false, nillable = false)
public String getRedirectUri( )
{
return redirectUri;
}
public void setRedirectUri( String redirectUri )
{
this.redirectUri = redirectUri;
}
@XmlElement(name="state", required = false, nillable = false)
public String getState( )
{
return state;
}
public void setState( String state )
{
this.state = state;
}
}

View File

@ -0,0 +1,146 @@
package org.apache.archiva.redback.rest.api.model;
/*
* 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.redback.authentication.Token;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.time.Duration;
import java.time.Instant;
/**
* @author Martin Stockhammer <martin_s@apache.org>
*/
@XmlRootElement(name="token")
public class TokenResponse
{
String accessToken;
String tokenType = "bearer";
long expiresIn;
String refreshToken;
String scope;
String state;
public TokenResponse( )
{
}
public TokenResponse( String accessToken, String tokenType, long expiresIn, String refreshToken, String scope )
{
this.accessToken = accessToken;
this.tokenType = tokenType;
this.expiresIn = expiresIn;
this.refreshToken = refreshToken;
this.scope = scope;
}
public TokenResponse( String accessToken, long expiresIn, String refreshToken, String scope )
{
this.accessToken = accessToken;
this.expiresIn = expiresIn;
this.refreshToken = refreshToken;
this.scope = scope;
}
public TokenResponse( Token accessToken, Token refreshToken )
{
this.expiresIn = Duration.between( Instant.now( ), accessToken.getMetadata( ).validBefore( ) ).getSeconds();
this.accessToken = accessToken.getData( );
this.refreshToken = refreshToken.getData( );
this.scope = "";
}
public TokenResponse( Token accessToken, Token refreshToken , String scope, String state)
{
this.expiresIn = Duration.between( Instant.now( ), accessToken.getMetadata( ).validBefore( ) ).getSeconds();
this.accessToken = accessToken.getData( );
this.refreshToken = refreshToken.getData( );
this.scope = scope;
this.state = state;
}
@XmlElement(name="access_token")
public String getAccessToken( )
{
return accessToken;
}
public void setAccessToken( String accessToken )
{
this.accessToken = accessToken;
}
@XmlElement(name="token_type")
public String getTokenType( )
{
return tokenType;
}
public void setTokenType( String tokenType )
{
this.tokenType = tokenType;
}
@XmlElement(name="expires_in")
public long getExpiresIn( )
{
return expiresIn;
}
public void setExpiresIn( long expiresIn )
{
this.expiresIn = expiresIn;
}
@XmlElement(name="refresh_token")
public String getRefreshToken( )
{
return refreshToken;
}
public void setRefreshToken( String refreshToken )
{
this.refreshToken = refreshToken;
}
public String getScope( )
{
return scope;
}
public void setScope( String scope )
{
this.scope = scope;
}
public String getState( )
{
return state;
}
public void setState( String state )
{
this.state = state;
}
public boolean hasState() {
return state != null && state.length( ) > 0;
}
}

View File

@ -56,7 +56,7 @@ public interface LoginService
@GET @GET
@Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN } ) @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN } )
@RedbackAuthorization( noRestriction = true ) @RedbackAuthorization( noRestriction = true )
PingResult ping() Boolean ping()
throws RedbackServiceException; throws RedbackServiceException;
@ -65,7 +65,7 @@ public interface LoginService
@GET @GET
@Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN } ) @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN } )
@RedbackAuthorization( noRestriction = false, noPermission = true ) @RedbackAuthorization( noRestriction = false, noPermission = true )
PingResult pingWithAutz() Boolean pingWithAutz()
throws RedbackServiceException; throws RedbackServiceException;
/** /**

View File

@ -151,7 +151,7 @@ public interface UserService
@GET @GET
@Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN } ) @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN } )
@RedbackAuthorization( noRestriction = true ) @RedbackAuthorization( noRestriction = true )
PingResult ping() Boolean ping()
throws RedbackServiceException; throws RedbackServiceException;
@Path( "removeFromCache/{userName}" ) @Path( "removeFromCache/{userName}" )

View File

@ -25,7 +25,10 @@ import org.apache.archiva.redback.authorization.RedbackAuthorization;
import org.apache.archiva.redback.rest.api.model.ActionStatus; import org.apache.archiva.redback.rest.api.model.ActionStatus;
import org.apache.archiva.redback.rest.api.model.LoginRequest; import org.apache.archiva.redback.rest.api.model.LoginRequest;
import org.apache.archiva.redback.rest.api.model.PingResult; import org.apache.archiva.redback.rest.api.model.PingResult;
import org.apache.archiva.redback.rest.api.model.RefreshTokenRequest;
import org.apache.archiva.redback.rest.api.model.RequestTokenRequest;
import org.apache.archiva.redback.rest.api.model.Token; import org.apache.archiva.redback.rest.api.model.Token;
import org.apache.archiva.redback.rest.api.model.TokenResponse;
import org.apache.archiva.redback.rest.api.model.User; import org.apache.archiva.redback.rest.api.model.User;
import org.apache.archiva.redback.rest.api.services.RedbackServiceException; import org.apache.archiva.redback.rest.api.services.RedbackServiceException;
@ -43,17 +46,6 @@ import javax.ws.rs.core.MediaType;
public interface AuthenticationService public interface AuthenticationService
{ {
@Path( "requestkey" )
@GET
@Produces( { MediaType.APPLICATION_JSON } )
@RedbackAuthorization( noRestriction = true )
Token requestOnetimeToken( @QueryParam( "providerKey" ) String providedKey,
@QueryParam( "principal" ) String principal,
@QueryParam( "purpose" ) String purpose,
@QueryParam( "expirationSeconds" ) int expirationSeconds )
throws RedbackServiceException;
@Path( "ping" ) @Path( "ping" )
@GET @GET
@Produces( { MediaType.APPLICATION_JSON } ) @Produces( { MediaType.APPLICATION_JSON } )
@ -74,7 +66,7 @@ public interface AuthenticationService
* The bearer token can be added to the HTTP header on further requests to authenticate. * The bearer token can be added to the HTTP header on further requests to authenticate.
* *
*/ */
@Path( "authenticate" ) @Path( "token" )
@POST @POST
@RedbackAuthorization( noRestriction = true, noPermission = true ) @RedbackAuthorization( noRestriction = true, noPermission = true )
@Produces( { MediaType.APPLICATION_JSON } ) @Produces( { MediaType.APPLICATION_JSON } )
@ -83,23 +75,23 @@ public interface AuthenticationService
@ApiResponse( description = "The bearer token. The token data contains the token string that should be added to the Bearer header" ) @ApiResponse( description = "The bearer token. The token data contains the token string that should be added to the Bearer header" )
} }
) )
Token logIn( LoginRequest loginRequest ) TokenResponse logIn( RequestTokenRequest loginRequest )
throws RedbackServiceException; throws RedbackServiceException;
/** /**
* Renew the bearer token. The request must send a bearer token in the HTTP header * Renew the bearer token. The request must send a bearer token in the HTTP header
* *
*/ */
@Path( "authenticate" ) @Path( "refresh" )
@GET @POST
@RedbackAuthorization( noRestriction = false, noPermission = true ) @RedbackAuthorization( noRestriction = false, noPermission = true )
@Produces( { MediaType.APPLICATION_JSON } ) @Produces( { MediaType.APPLICATION_JSON } )
@Operation( summary = "Creates a new bearer token. The requestor must present a still valid bearer token in the HTTP header.", @Operation( summary = "Creates a new bearer token. The requester must present a still valid bearer token in the HTTP header.",
responses = { responses = {
@ApiResponse( description = "The new bearer token," ) @ApiResponse( description = "The new bearer token," )
} }
) )
Token renewToken( ) TokenResponse refreshToken( RefreshTokenRequest refreshTokenRequest )
throws RedbackServiceException; throws RedbackServiceException;
@ -107,21 +99,11 @@ public interface AuthenticationService
* simply check if current user has an http session opened with authz passed and return user data * simply check if current user has an http session opened with authz passed and return user data
* @since 1.4 * @since 1.4
*/ */
@Path( "isAuthenticated" ) @Path( "authenticated" )
@GET @GET
@Produces( { MediaType.APPLICATION_JSON } ) @Produces( { MediaType.APPLICATION_JSON } )
@RedbackAuthorization( noRestriction = true ) @RedbackAuthorization( noRestriction = true )
User isLogged() User getAuthenticatedUser()
throws RedbackServiceException; throws RedbackServiceException;
/**
* clear user http session
* @since 1.4
*/
@Path( "logout" )
@GET
@Produces( { MediaType.APPLICATION_JSON } )
@RedbackAuthorization( noRestriction = true, noPermission = true )
ActionStatus logout()
throws RedbackServiceException;
} }

View File

@ -126,16 +126,16 @@ public class DefaultLoginService
return key.getKey( ); return key.getKey( );
} }
public PingResult ping() public Boolean ping()
throws RedbackServiceException throws RedbackServiceException
{ {
return new PingResult( true); return Boolean.TRUE;
} }
public PingResult pingWithAutz() public Boolean pingWithAutz()
throws RedbackServiceException throws RedbackServiceException
{ {
return new PingResult( true ); return Boolean.TRUE;
} }
public User logIn( LoginRequest loginRequest ) public User logIn( LoginRequest loginRequest )

View File

@ -490,10 +490,10 @@ public class DefaultUserService
} }
@Override @Override
public PingResult ping() public Boolean ping()
throws RedbackServiceException throws RedbackServiceException
{ {
return new PingResult( true ); return Boolean.TRUE;
} }
private User getSimpleUser( org.apache.archiva.redback.users.User user ) private User getSimpleUser( org.apache.archiva.redback.users.User user )

View File

@ -23,20 +23,18 @@ import org.apache.archiva.redback.authentication.AuthenticationConstants;
import org.apache.archiva.redback.authentication.AuthenticationException; import org.apache.archiva.redback.authentication.AuthenticationException;
import org.apache.archiva.redback.authentication.AuthenticationFailureCause; import org.apache.archiva.redback.authentication.AuthenticationFailureCause;
import org.apache.archiva.redback.authentication.PasswordBasedAuthenticationDataSource; import org.apache.archiva.redback.authentication.PasswordBasedAuthenticationDataSource;
import org.apache.archiva.redback.authentication.Token;
import org.apache.archiva.redback.authentication.TokenType;
import org.apache.archiva.redback.authentication.jwt.JwtAuthenticator; import org.apache.archiva.redback.authentication.jwt.JwtAuthenticator;
import org.apache.archiva.redback.authentication.jwt.TokenAuthenticationException;
import org.apache.archiva.redback.integration.filter.authentication.HttpAuthenticator; import org.apache.archiva.redback.integration.filter.authentication.HttpAuthenticator;
import org.apache.archiva.redback.keys.AuthenticationKey;
import org.apache.archiva.redback.keys.KeyManager;
import org.apache.archiva.redback.keys.jpa.model.JpaAuthenticationKey;
import org.apache.archiva.redback.keys.memory.MemoryAuthenticationKey;
import org.apache.archiva.redback.keys.memory.MemoryKeyManager;
import org.apache.archiva.redback.policy.AccountLockedException; import org.apache.archiva.redback.policy.AccountLockedException;
import org.apache.archiva.redback.policy.MustChangePasswordException; import org.apache.archiva.redback.policy.MustChangePasswordException;
import org.apache.archiva.redback.rest.api.model.ActionStatus;
import org.apache.archiva.redback.rest.api.model.ErrorMessage; import org.apache.archiva.redback.rest.api.model.ErrorMessage;
import org.apache.archiva.redback.rest.api.model.LoginRequest;
import org.apache.archiva.redback.rest.api.model.PingResult; import org.apache.archiva.redback.rest.api.model.PingResult;
import org.apache.archiva.redback.rest.api.model.Token; import org.apache.archiva.redback.rest.api.model.RefreshTokenRequest;
import org.apache.archiva.redback.rest.api.model.RequestTokenRequest;
import org.apache.archiva.redback.rest.api.model.TokenResponse;
import org.apache.archiva.redback.rest.api.model.User; import org.apache.archiva.redback.rest.api.model.User;
import org.apache.archiva.redback.rest.api.model.UserLogin; import org.apache.archiva.redback.rest.api.model.UserLogin;
import org.apache.archiva.redback.rest.api.services.RedbackServiceException; import org.apache.archiva.redback.rest.api.services.RedbackServiceException;
@ -53,17 +51,11 @@ import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.TimeZone;
/** /**
* *
@ -105,37 +97,6 @@ public class DefaultAuthenticationService
} }
@Override
public Token requestOnetimeToken( String providedKey, String principal, String purpose, int expirationSeconds )
{
KeyManager keyManager = securitySystem.getKeyManager();
AuthenticationKey key;
if ( keyManager instanceof MemoryKeyManager )
{
key = new MemoryAuthenticationKey();
}
else
{
key = new JpaAuthenticationKey();
}
key.setKey( providedKey );
key.setForPrincipal( principal );
key.setPurpose( purpose );
Instant now = Instant.now( );
key.setDateCreated( Date.from( now ) );
if ( expirationSeconds >= 0 )
{
Duration expireDuration = Duration.ofSeconds( expirationSeconds );
key.setDateExpires( Date.from( now.plus( expireDuration ) ) );
}
keyManager.addKey( key );
return Token.of( key );
}
@Override @Override
public PingResult ping() public PingResult ping()
{ {
@ -148,15 +109,11 @@ public class DefaultAuthenticationService
return new PingResult( true ); return new PingResult( true );
} }
private Token getRestToken( org.apache.archiva.redback.authentication.Token token ) {
return Token.of( token.getData( ), token.getMetadata( ).created( ), token.getMetadata( ).validBefore( ), token.getMetadata( ).getUser( ), "rest-auth" );
}
@Override @Override
public Token logIn( LoginRequest loginRequest ) public TokenResponse logIn( RequestTokenRequest loginRequest )
throws RedbackServiceException throws RedbackServiceException
{ {
String userName = loginRequest.getUsername(), password = loginRequest.getPassword(); String userName = loginRequest.getUserId(), password = loginRequest.getPassword();
PasswordBasedAuthenticationDataSource authDataSource = PasswordBasedAuthenticationDataSource authDataSource =
new PasswordBasedAuthenticationDataSource( userName, password ); new PasswordBasedAuthenticationDataSource( userName, password );
log.debug("Login for {}",userName); log.debug("Login for {}",userName);
@ -177,8 +134,10 @@ public class DefaultAuthenticationService
} }
// Stateless services no session // Stateless services no session
// httpAuthenticator.authenticate( authDataSource, httpServletRequest.getSession( true ) ); // httpAuthenticator.authenticate( authDataSource, httpServletRequest.getSession( true ) );
Token restToken = getRestToken( token ); org.apache.archiva.redback.authentication.Token refreshToken = jwtAuthenticator.generateToken( user.getUsername( ), TokenType.REFRESH_TOKEN );
return restToken; response.setHeader( "Cache-Control", "no-store" );
response.setHeader( "Pragma", "no-cache" );
return new TokenResponse(token, refreshToken, "", loginRequest.getState());
} else if ( securitySession.getAuthenticationResult() != null } else if ( securitySession.getAuthenticationResult() != null
&& securitySession.getAuthenticationResult().getAuthenticationFailureCauses() != null ) && securitySession.getAuthenticationResult().getAuthenticationFailureCauses() != null )
{ {
@ -231,13 +190,25 @@ public class DefaultAuthenticationService
} }
@Override @Override
public Token renewToken( ) throws RedbackServiceException public TokenResponse refreshToken( RefreshTokenRequest request ) throws RedbackServiceException
{ {
return null; if (!"refresh_token".equals(request.getGrantType().toLowerCase())) {
throw new RedbackServiceException( "redback:bad_grant", Response.Status.FORBIDDEN.getStatusCode( ) );
}
try
{
Token accessToken = jwtAuthenticator.refreshAccessToken( request.getRefreshToken( ) );
Token refreshToken = jwtAuthenticator.tokenFromString( request.getRefreshToken( ) );
return new TokenResponse( accessToken, refreshToken );
}
catch ( TokenAuthenticationException e )
{
throw new RedbackServiceException( e.getError( ).getError( ), Response.Status.UNAUTHORIZED.getStatusCode( ) );
}
} }
@Override @Override
public User isLogged() public User getAuthenticatedUser()
throws RedbackServiceException throws RedbackServiceException
{ {
SecuritySession securitySession = httpAuthenticator.getSecuritySession( httpServletRequest.getSession( true ) ); SecuritySession securitySession = httpAuthenticator.getSecuritySession( httpServletRequest.getSession( true ) );
@ -246,23 +217,6 @@ public class DefaultAuthenticationService
return isLogged && securitySession.getUser() != null ? buildRestUser( securitySession.getUser() ) : null; return isLogged && securitySession.getUser() != null ? buildRestUser( securitySession.getUser() ) : null;
} }
@Override
public ActionStatus logout()
throws RedbackServiceException
{
HttpSession httpSession = httpServletRequest.getSession();
if ( httpSession != null )
{
httpSession.invalidate();
}
return ActionStatus.SUCCESS;
}
private Calendar getNowGMT()
{
return Calendar.getInstance( TimeZone.getTimeZone( "GMT" ) );
}
private UserLogin buildRestUser( org.apache.archiva.redback.users.User user ) private UserLogin buildRestUser( org.apache.archiva.redback.users.User user )
{ {
UserLogin restUser = new UserLogin(); UserLogin restUser = new UserLogin();

View File

@ -44,6 +44,13 @@ public class LoginServiceTest
FakeCreateAdminService.ADMIN_TEST_PWD ) ) ); FakeCreateAdminService.ADMIN_TEST_PWD ) ) );
} }
@Test
public void ping()
throws Exception
{
assertNotNull( getLoginService( null ).ping( ) );
}
@Test @Test
public void createUserThenLog() public void createUserThenLog()
throws Exception throws Exception

View File

@ -57,7 +57,7 @@ public class UserServiceTest
public void ping() public void ping()
throws Exception throws Exception
{ {
Boolean res = getUserService().ping().isSuccess(); Boolean res = getUserService().ping();
assertTrue( res.booleanValue() ); assertTrue( res.booleanValue() );
} }

View File

@ -0,0 +1,289 @@
package org.apache.archiva.redback.rest.services.v2;
/*
* 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.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.specification.RequestSpecification;
import org.apache.archiva.redback.integration.security.role.RedbackRoleConstants;
import org.apache.archiva.redback.rest.services.FakeCreateAdminServiceImpl;
import org.apache.archiva.redback.role.RoleManager;
import org.apache.archiva.redback.role.RoleManagerException;
import org.apache.archiva.redback.users.User;
import org.apache.archiva.redback.users.UserManager;
import org.apache.archiva.redback.users.UserManagerException;
import org.apache.archiva.redback.users.UserNotFoundException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.cxf.transport.servlet.CXFServlet;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.jupiter.api.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.ContextLoaderListener;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static io.restassured.RestAssured.baseURI;
import static io.restassured.RestAssured.port;
import static org.junit.jupiter.api.Assertions.assertNotNull;
/**
*
* Native REST tests do not use the JAX-RS client and can be used with a remote
* REST API service. The tests
*
* @author Martin Stockhammer <martin_s@apache.org>
*/
@Tag("rest-native")
public abstract class AbstractNativeRestServices
{
public static final String SYSPROP_START_SERVER = "archiva.rest.start.server";
public static final String SYSPROP_SERVER_PORT = "archiva.rest.server.port";
public static final String SYSPROP_SERVER_BASE_URI = "archiva.rest.server.baseuri";
public static final int STOPPED = 0;
public static final int STOPPING = 1;
public static final int STARTING = 2;
public static final int STARTED = 3;
public static final int ERROR = 4;
private RequestSpecification requestSpec;
protected Logger log = LoggerFactory.getLogger( getClass() );
private static AtomicReference<Server> server = new AtomicReference<>();
private static AtomicReference<ServerConnector> serverConnector = new AtomicReference<>();
private static AtomicInteger serverStarted = new AtomicInteger( STOPPED );
private UserManager userManager;
private RoleManager roleManager;
protected abstract String getServicePath();
protected String getSpringConfigLocation()
{
return "classpath*:spring-context.xml,classpath*:META-INF/spring-context.xml";
}
protected RequestSpecification getRequestSpec() {
return this.requestSpec;
}
protected String getContextRoot()
{
return "/api";
}
private String getServiceBasePath( )
{
return "/v2/redback";
}
protected String getBasePath( )
{
return new StringBuilder( )
.append(getContextRoot( ))
.append(getServiceBasePath( ))
.append(getServicePath( )).toString();
}
/**
* Returns the server that was started, or null if not initialized before.
* @return
*/
public Server getServer() {
return this.server.get();
}
public int getServerPort() {
ServerConnector connector = serverConnector.get();
if (connector!=null) {
return connector.getLocalPort();
} else {
return 0;
}
}
/**
* Returns true, if the server does exist and is running.
* @return true, if server does exist and is running.
*/
public boolean isServerRunning() {
return serverStarted.get()==STARTED && this.server.get() != null && this.server.get().isRunning();
}
private UserManager getUserManager() {
if (this.userManager==null) {
UserManager userManager = ContextLoaderListener.getCurrentWebApplicationContext( )
.getBean( "userManager#default", UserManager.class );
assertNotNull( userManager );
this.userManager = userManager;
}
return this.userManager;
}
private RoleManager getRoleManager() {
if (this.roleManager==null) {
RoleManager roleManager = ContextLoaderListener.getCurrentWebApplicationContext( )
.getBean( "roleManager", RoleManager.class );
assertNotNull( roleManager );
this.roleManager = roleManager;
}
return this.roleManager;
}
private void setupAdminUser() throws UserManagerException, RoleManagerException
{
UserManager um = getUserManager( );
User adminUser = null;
try
{
adminUser = um.findUser( RedbackRoleConstants.ADMINISTRATOR_ACCOUNT_NAME );
} catch ( UserNotFoundException e ) {
// ignore
}
if (adminUser==null)
{
adminUser = um.createUser( RedbackRoleConstants.ADMINISTRATOR_ACCOUNT_NAME, "Administrator", "admin@local.home" );
adminUser.setUsername( RedbackRoleConstants.ADMINISTRATOR_ACCOUNT_NAME );
adminUser.setPassword( FakeCreateAdminServiceImpl.ADMIN_TEST_PWD );
adminUser.setFullName( "the admin user" );
adminUser.setEmail( "toto@toto.fr" );
adminUser.setPermanent( true );
adminUser.setValidated( true );
adminUser.setLocked( false );
adminUser.setPasswordChangeRequired( false );
um.addUser( adminUser );
getRoleManager( ).assignRole( "system-administrator", adminUser.getUsername( ) );
}
}
public void startServer()
throws Exception
{
if (serverStarted.compareAndSet( STOPPED, STARTING ))
{
try
{
log.info( "Starting server" );
Server myServer = new Server( );
this.server.set( myServer );
this.serverConnector.set( new ServerConnector( myServer, new HttpConnectionFactory( ) ) );
myServer.addConnector( serverConnector.get( ) );
ServletHolder servletHolder = new ServletHolder( new CXFServlet( ) );
ServletContextHandler context = new ServletContextHandler( ServletContextHandler.SESSIONS );
context.setResourceBase( SystemUtils.JAVA_IO_TMPDIR );
context.setSessionHandler( new SessionHandler( ) );
context.addServlet( servletHolder, getContextRoot( ) + "/*" );
context.setInitParameter( "contextConfigLocation", getSpringConfigLocation( ) );
context.addEventListener( new ContextLoaderListener( ) );
getServer( ).setHandler( context );
getServer( ).start( );
if ( log.isDebugEnabled( ) )
{
log.debug( "Jetty dump: {}", getServer( ).dump( ) );
}
setupAdminUser();
log.info( "Started server on port {}", getServerPort( ) );
serverStarted.set( STARTED );
} finally {
// In case, if the last statement was not reached
serverStarted.compareAndSet( STARTING, ERROR );
}
}
}
public void stopServer()
throws Exception
{
if ( this.serverStarted.compareAndSet( STARTED, STOPPING ) )
{
try
{
final Server myServer = getServer( );
if ( myServer != null )
{
log.info("Stopping server");
myServer.stop();
}
serverStarted.set( STOPPED );
} finally {
serverStarted.compareAndSet( STOPPING, ERROR );
}
} else {
log.error( "Serer is not in STARTED state!" );
}
}
protected void setupNative( ) throws Exception
{
String startServer = System.getProperty( SYSPROP_START_SERVER, "yes" ).toLowerCase( );
String serverPort = System.getProperty( SYSPROP_SERVER_PORT, "" );
String baseUri = System.getProperty( SYSPROP_SERVER_BASE_URI, "http://localhost" );
if ( !"no".equals( startServer ) )
{
startServer( );
}
if ( StringUtils.isNotEmpty( serverPort ) )
{
RestAssured.port = Integer.parseInt( serverPort );
}
else
{
RestAssured.port = getServerPort( );
}
if ( StringUtils.isNotEmpty( baseUri ) )
{
RestAssured.baseURI = baseUri;
}
else
{
RestAssured.baseURI = "http://localhost";
}
String basePath = getBasePath( );
RequestSpecBuilder builder = new RequestSpecBuilder( );
builder.setBaseUri( baseURI )
.setPort( port )
.setBasePath( basePath )
.addHeader( "Origin", RestAssured.baseURI + ":" + RestAssured.port );
this.requestSpec = builder.build( );
RestAssured.basePath = basePath;
}
protected void shutdownNative() throws Exception
{
stopServer();
}
}

View File

@ -22,10 +22,10 @@ package org.apache.archiva.redback.rest.services.v2;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
import org.apache.archiva.redback.authentication.Token; import org.apache.archiva.redback.authentication.Token;
import org.apache.archiva.redback.authentication.jwt.JwtAuthenticator; import org.apache.archiva.redback.authentication.jwt.JwtAuthenticator;
import org.apache.archiva.redback.integration.security.role.RedbackRoleConstants; import org.apache.archiva.redback.integration.security.role.RedbackRoleConstants;
import org.apache.archiva.redback.rest.api.services.RoleManagementService;
import org.apache.archiva.redback.rest.api.services.v2.AuthenticationService; import org.apache.archiva.redback.rest.api.services.v2.AuthenticationService;
import org.apache.archiva.redback.rest.services.FakeCreateAdminService; import org.apache.archiva.redback.rest.services.FakeCreateAdminService;
import org.apache.archiva.redback.rest.services.FakeCreateAdminServiceImpl; import org.apache.archiva.redback.rest.services.FakeCreateAdminServiceImpl;
@ -56,6 +56,7 @@ import javax.naming.NameNotFoundException;
import javax.naming.NamingException; import javax.naming.NamingException;
import javax.naming.directory.DirContext; import javax.naming.directory.DirContext;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import java.text.SimpleDateFormat;
import java.util.Collections; import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@ -137,6 +138,8 @@ public abstract class AbstractRestServicesTestV2
JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider( ); JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider( );
ObjectMapper mapper = new ObjectMapper( ); ObjectMapper mapper = new ObjectMapper( );
mapper.registerModule( new JavaTimeModule( ) ); mapper.registerModule( new JavaTimeModule( ) );
mapper.setAnnotationIntrospector( new JaxbAnnotationIntrospector( mapper.getTypeFactory() ) );
mapper.setDateFormat( new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ" ) );
provider.setMapper( mapper ); provider.setMapper( mapper );
return provider; return provider;
} }
@ -227,7 +230,7 @@ public abstract class AbstractRestServicesTestV2
protected String getRestServicesPath() protected String getRestServicesPath()
{ {
return "restServices"; return "api";
} }
public void startServer() public void startServer()

View File

@ -20,7 +20,9 @@ package org.apache.archiva.redback.rest.services.v2;
import org.apache.archiva.redback.integration.security.role.RedbackRoleConstants; import org.apache.archiva.redback.integration.security.role.RedbackRoleConstants;
import org.apache.archiva.redback.rest.api.model.LoginRequest; import org.apache.archiva.redback.rest.api.model.LoginRequest;
import org.apache.archiva.redback.rest.api.model.RequestTokenRequest;
import org.apache.archiva.redback.rest.api.model.Token; import org.apache.archiva.redback.rest.api.model.Token;
import org.apache.archiva.redback.rest.api.model.TokenResponse;
import org.apache.archiva.redback.rest.api.services.RedbackServiceException; import org.apache.archiva.redback.rest.api.services.RedbackServiceException;
import org.apache.archiva.redback.rest.api.services.UserService; import org.apache.archiva.redback.rest.api.services.UserService;
import org.apache.archiva.redback.rest.services.FakeCreateAdminService; import org.apache.archiva.redback.rest.services.FakeCreateAdminService;
@ -64,7 +66,7 @@ public class AuthenticationServiceTest
public void loginAdmin() public void loginAdmin()
throws Exception throws Exception
{ {
assertNotNull( getLoginServiceV2( null ).logIn( new LoginRequest( RedbackRoleConstants.ADMINISTRATOR_ACCOUNT_NAME, assertNotNull( getLoginServiceV2( null ).logIn( new RequestTokenRequest( RedbackRoleConstants.ADMINISTRATOR_ACCOUNT_NAME,
FakeCreateAdminService.ADMIN_TEST_PWD ) ) ); FakeCreateAdminService.ADMIN_TEST_PWD ) ) );
} }
@ -117,8 +119,8 @@ public class AuthenticationServiceTest
user.setPasswordChangeRequired( false ); user.setPasswordChangeRequired( false );
um.updateUser( user ); um.updateUser( user );
// END SNIPPET: create-user // END SNIPPET: create-user
LoginRequest request = new LoginRequest( "toto", "foo123" ); RequestTokenRequest request = new RequestTokenRequest( "toto", "foo123" );
Token result = getLoginServiceV2( "" ).logIn( request ); TokenResponse result = getLoginServiceV2( "" ).logIn( request );
// assertNotNull( result ); // assertNotNull( result );
// assertEquals( "toto", result.getUsername( ) ); // assertEquals( "toto", result.getUsername( ) );

View File

@ -0,0 +1,90 @@
package org.apache.archiva.redback.rest.services.v2;
/*
* 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.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.response.Response;
import io.restassured.specification.RequestSpecification;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.time.Instant;
import java.time.OffsetDateTime;
import static io.restassured.RestAssured.*;
import static io.restassured.http.ContentType.JSON;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.core.annotation.MergedAnnotations.from;
/**
* @author Martin Stockhammer <martin_s@apache.org>
*/
@ExtendWith( SpringExtension.class )
@ContextConfiguration(
locations = {"classpath:/ldap-spring-test.xml"} )
@TestInstance( TestInstance.Lifecycle.PER_CLASS )
@Tag( "rest-native" )
public class NativeAuthenticationServiceTest extends AbstractNativeRestServices
{
@Override
protected String getServicePath( )
{
return "/auth";
}
@BeforeAll
void setup( ) throws Exception
{
setupNative( );
}
@AfterAll
void shutdown( ) throws Exception
{
shutdownNative();
}
@Test
void ping( )
{
Instant beforeCall = Instant.now( );
Response response = given( ).spec( getRequestSpec() )
.when( ).get( "/ping" )
.then( ).assertThat( ).statusCode( 200 ).and( )
.contentType( JSON ).
body( "success", equalTo( true ) )
.body( "requestTime", notNullValue( ) ).extract().response();
OffsetDateTime dateTime = OffsetDateTime.parse( response.body( ).jsonPath( ).getString( "requestTime" ) );
Instant afterCall = Instant.now( );
assertTrue( dateTime.toInstant( ).isAfter( beforeCall ) );
assertTrue( dateTime.toInstant( ).isBefore( afterCall ) );
}
}

View File

@ -31,7 +31,7 @@
<logger name="org.springframework" level="error"/> <logger name="org.springframework" level="error"/>
<logger name="org.apache.archiva.redback.components.cache" level="error"/> <logger name="org.apache.archiva.redback.components.cache" level="error"/>
<logger name="org.apache.archiva.redback.rest.services.interceptors" level="debug"/> <logger name="org.apache.archiva.redback.rest.services.interceptors" level="debug"/>
<logger name="org.apache.archiva.redback.rest.services" level="info"/> <logger name="org.apache.archiva.redback.rest.services" level="debug"/>
<logger name="org.apache.catalina" level="off" /> <logger name="org.apache.catalina" level="off" />
<logger name="JPOX" level="ERROR"/> <logger name="JPOX" level="ERROR"/>
<root level="info"> <root level="info">