From 15ad042ac1cf1d951f235f3314106092d74f33b5 Mon Sep 17 00:00:00 2001 From: Martin Stockhammer Date: Sun, 12 Jul 2020 21:03:37 +0200 Subject: [PATCH] Adding refresh token for authentication --- .../authentication/SimpleTokenData.java | 16 + .../redback/authentication/StringToken.java | 33 +- .../archiva/redback/authentication/Token.java | 25 ++ .../redback/authentication/TokenType.java | 48 +++ .../authentication/jwt/JwtAuthenticator.java | 135 ++++++-- .../authentication/jwt/AbstractJwtTest.java | 35 ++- .../configuration/UserConfigurationKeys.java | 19 +- .../redback/rest/api/model/PingResult.java | 15 +- .../rest/api/model/RefreshTokenRequest.java | 77 +++++ .../rest/api/model/RequestTokenRequest.java | 156 ++++++++++ .../redback/rest/api/model/TokenResponse.java | 146 +++++++++ .../rest/api/services/LoginService.java | 4 +- .../rest/api/services/UserService.java | 2 +- .../services/v2/AuthenticationService.java | 40 +-- .../rest/services/DefaultLoginService.java | 8 +- .../rest/services/DefaultUserService.java | 4 +- .../v2/DefaultAuthenticationService.java | 100 ++---- .../rest/services/LoginServiceTest.java | 7 + .../rest/services/UserServiceTest.java | 2 +- .../v2/AbstractNativeRestServices.java | 289 ++++++++++++++++++ .../v2/AbstractRestServicesTestV2.java | 7 +- .../v2/AuthenticationServiceTest.java | 8 +- .../v2/NativeAuthenticationServiceTest.java | 90 ++++++ .../src/test/resources/log4j2-test.xml | 2 +- 24 files changed, 1111 insertions(+), 157 deletions(-) create mode 100644 redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenType.java create mode 100644 redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/RefreshTokenRequest.java create mode 100644 redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/RequestTokenRequest.java create mode 100644 redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/TokenResponse.java create mode 100644 redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AbstractNativeRestServices.java create mode 100644 redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeAuthenticationServiceTest.java diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java index fc0de018..e5c25535 100644 --- a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java @@ -61,6 +61,22 @@ public final class SimpleTokenData implements Serializable, TokenData { 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 public final String getUser() { return user; diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/StringToken.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/StringToken.java index c96c4e29..e4ab1664 100644 --- a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/StringToken.java +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/StringToken.java @@ -26,23 +26,34 @@ package org.apache.archiva.redback.authentication; public class StringToken implements Token { final TokenData metadata; - final String token; + final String data; + final String id; + final TokenType type; - public StringToken(String tokenData, TokenData metadata) { - this.token = tokenData; + public StringToken(String id, String tokenData, TokenData metadata) { + this.id = id; + this.data = tokenData; 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 public String getData( ) { - return token; + return data; } @Override public byte[] getBytes( ) { - return token.getBytes( ); + return data.getBytes( ); } @Override @@ -50,4 +61,16 @@ public class StringToken implements Token { return metadata; } + + @Override + public String getId( ) + { + return id; + } + + @Override + public TokenType getType( ) + { + return type; + } } diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/Token.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/Token.java index 221a57b5..56b6666e 100644 --- a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/Token.java +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/Token.java @@ -26,9 +26,34 @@ package org.apache.archiva.redback.authentication; 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(); + /** + * The token as byte array + * @return + */ byte[] getBytes(); + /** + * The token meta data, like expiration time. + * @return the metadata + */ TokenData getMetadata(); } diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenType.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenType.java new file mode 100644 index 00000000..9441d089 --- /dev/null +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenType.java @@ -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 + */ +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; + } +} diff --git a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticator.java b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticator.java index b67a1344..de34f943 100644 --- a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticator.java +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/main/java/org/apache/archiva/redback/authentication/jwt/JwtAuthenticator.java @@ -21,12 +21,14 @@ package org.apache.archiva.redback.authentication.jwt; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.IncorrectClaimException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.MissingClaimException; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SigningKeyResolverAdapter; 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.TokenBasedAuthenticationDataSource; 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.UserConfigurationKeys; import org.apache.commons.lang3.StringUtils; @@ -78,9 +81,11 @@ import java.time.Instant; import java.util.Arrays; import java.util.Base64; import java.util.Date; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Properties; +import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.locks.ReadWriteLock; 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 ); + // 4 hours for standard tokens 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 ID = "JwtAuthenticator"; 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_KEYID = "keyId"; private static final String ISSUER = "archiva.apache.org/redback"; + private static final String TOKEN_TYPE = "token_type"; @Inject @@ -168,9 +177,14 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic AtomicLong keyCounter; final SigningKeyResolver resolver = new SigningKeyResolver( ); final ReadWriteLock lock = new ReentrantReadWriteLock( ); - private JwtParser parser; - private Duration lifetime; + private Duration tokenLifetime; + private Duration refreshTokenLifetime; + private Map parserMap = new HashMap<>( ); + + private JwtParser getParser(TokenType type) { + return parserMap.get( type ); + } public class SigningKeyResolver extends SigningKeyResolverAdapter { @@ -242,12 +256,24 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic // In memory key store is the default addNewKey( ); } - this.parser = Jwts.parserBuilder( ) + this.parserMap.put(TokenType.ALL, Jwts.parserBuilder( ) .setSigningKeyResolver( getResolver( ) ) .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 ) @@ -661,33 +687,94 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic { final KeyHolder signerKey = getSignerKey( ); Instant now = Instant.now( ); - Instant expiration = now.plus( lifetime ); + Instant expiration = now.plus( tokenLifetime ); final String token = Jwts.builder( ) .setSubject( userId ) .setIssuer( ISSUER ) + .claim( TOKEN_TYPE, TokenType.ACCESS_TOKEN.getClaim( ) ) .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 ); + TokenData metadata = new SimpleTokenData( userId, tokenLifetime, 0 ); + return new StringToken("", token, metadata ); + } + + /** + * Creates a token for the given user id. The token contains the following data: + *
    + *
  • the userid as subject
  • + *
  • a issuer archiva.apache.org/redback
  • + *
  • a id header with the key id
  • + *
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 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 origin * 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 * @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 { - Jws signature = this.parser.parseClaimsJws( origin ); - return generateToken( signature.getBody( ).getSubject( ) ); - } catch (JwtException e) { - throw new AuthenticationException( "Could not renew the token " + e.getMessage( ) ); + String subject = verify( refreshToken, TokenType.REFRESH_TOKEN ); + return generateToken( subject ); + } catch ( JwtException e) { + 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 */ public Jws 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 - * If the verification failed a AuthenticationException is thrown. + * If the verification failed a TokenAuthenticationException is thrown. * @param token the JWT representation * @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 + { + return verify( token, TokenType.ACCESS_TOKEN ); + } + + public String verify( String token, TokenType type ) throws TokenAuthenticationException { try { - Jws signature = this.parser.parseClaimsJws( token ); + Jws signature = getParser(type).parseClaimsJws( token ); String subject = signature.getBody( ).getSubject( ); if ( StringUtils.isEmpty( subject ) ) { @@ -738,6 +830,9 @@ public class JwtAuthenticator extends AbstractAuthenticator implements Authentic catch (JwtKeyIdNotFoundException e) { 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) { log.debug( "Unknown JwtException {}, {}", e.getClass( ), 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 */ 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 */ public void setTokenLifetime(Duration lifetime) { - this.lifetime = lifetime; + this.tokenLifetime = lifetime; } public UserConfiguration getUserConfiguration( ) diff --git a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/AbstractJwtTest.java b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/AbstractJwtTest.java index 6acf4a0a..3ff71282 100644 --- a/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/AbstractJwtTest.java +++ b/redback-authentication/redback-authentication-providers/redback-authentication-jwt/src/test/java/org/apache/archiva/redback/authentication/jwt/AbstractJwtTest.java @@ -29,6 +29,7 @@ import org.apache.archiva.redback.authentication.BearerTokenAuthenticationDataSo 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.authentication.TokenType; import org.apache.archiva.redback.configuration.DefaultUserConfiguration; import org.apache.archiva.redback.configuration.UserConfigurationException; import org.apache.commons.configuration2.BaseConfiguration; @@ -39,6 +40,7 @@ import org.junit.jupiter.api.Test; import java.time.Duration; import java.time.Instant; import java.util.Map; +import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; @@ -119,12 +121,6 @@ public abstract class AbstractJwtTest assertTrue( Instant.now( ).isBefore( token.getMetadata( ).validBefore( ) ) ); } - - @Test - void authenticate( ) - { - } - @Test void renewSigningKey( ) { @@ -231,5 +227,32 @@ public abstract class AbstractJwtTest 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( ) ) ); + } + } diff --git a/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java b/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java index a54ab4df..f1c6e87b 100644 --- a/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java +++ b/redback-configuration/src/main/java/org/apache/archiva/redback/configuration/UserConfigurationKeys.java @@ -187,25 +187,25 @@ public interface UserConfigurationKeys 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 * {@value} */ 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"; /** - * Keystore type name for plain file keystore: {@value} + * The property value for plain file keystore: {@value} */ 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"; /** - * 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"; @@ -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"; /** - * 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"; + /** + * The property for lifetime in ms of the generated refresh tokens: {@value} + */ + String AUTHENTICATION_JWT_REFRESH_LIFETIME_MS = "authentication.jwt.refreshLifetimeMs"; + } diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/PingResult.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/PingResult.java index 34bde926..c8527359 100644 --- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/PingResult.java +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/PingResult.java @@ -19,6 +19,7 @@ package org.apache.archiva.redback.rest.api.model; */ import javax.xml.bind.annotation.XmlRootElement; +import java.time.OffsetDateTime; /** * @author Martin Stockhammer @@ -27,13 +28,15 @@ import javax.xml.bind.annotation.XmlRootElement; public class PingResult { boolean success; + OffsetDateTime requestTime; public PingResult() { - + this.requestTime = OffsetDateTime.now( ); } public PingResult( boolean success ) { this.success = success; + this.requestTime = OffsetDateTime.now( ); } public boolean isSuccess( ) @@ -45,4 +48,14 @@ public class PingResult { this.success = success; } + + public OffsetDateTime getRequestTime( ) + { + return requestTime; + } + + public void setRequestTime( OffsetDateTime requestTime ) + { + this.requestTime = requestTime; + } } diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/RefreshTokenRequest.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/RefreshTokenRequest.java new file mode 100644 index 00000000..a0302c89 --- /dev/null +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/RefreshTokenRequest.java @@ -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 + */ +@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; + } +} diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/RequestTokenRequest.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/RequestTokenRequest.java new file mode 100644 index 00000000..470344da --- /dev/null +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/RequestTokenRequest.java @@ -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 + */ +@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; + } +} diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/TokenResponse.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/TokenResponse.java new file mode 100644 index 00000000..6c9d4279 --- /dev/null +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/TokenResponse.java @@ -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 + */ +@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; + } +} diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/LoginService.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/LoginService.java index 6f8b4482..6ecf666b 100644 --- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/LoginService.java +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/LoginService.java @@ -56,7 +56,7 @@ public interface LoginService @GET @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN } ) @RedbackAuthorization( noRestriction = true ) - PingResult ping() + Boolean ping() throws RedbackServiceException; @@ -65,7 +65,7 @@ public interface LoginService @GET @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN } ) @RedbackAuthorization( noRestriction = false, noPermission = true ) - PingResult pingWithAutz() + Boolean pingWithAutz() throws RedbackServiceException; /** diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/UserService.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/UserService.java index 0363c852..1fbc633a 100644 --- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/UserService.java +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/UserService.java @@ -151,7 +151,7 @@ public interface UserService @GET @Produces( { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN } ) @RedbackAuthorization( noRestriction = true ) - PingResult ping() + Boolean ping() throws RedbackServiceException; @Path( "removeFromCache/{userName}" ) diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java index 88c8f4d4..cf75395d 100644 --- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/services/v2/AuthenticationService.java @@ -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.LoginRequest; 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.TokenResponse; import org.apache.archiva.redback.rest.api.model.User; import org.apache.archiva.redback.rest.api.services.RedbackServiceException; @@ -43,17 +46,6 @@ import javax.ws.rs.core.MediaType; 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" ) @GET @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. * */ - @Path( "authenticate" ) + @Path( "token" ) @POST @RedbackAuthorization( noRestriction = true, noPermission = true ) @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" ) } ) - Token logIn( LoginRequest loginRequest ) + TokenResponse logIn( RequestTokenRequest loginRequest ) throws RedbackServiceException; /** * Renew the bearer token. The request must send a bearer token in the HTTP header * */ - @Path( "authenticate" ) - @GET + @Path( "refresh" ) + @POST @RedbackAuthorization( noRestriction = false, noPermission = true ) @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 = { @ApiResponse( description = "The new bearer token," ) } ) - Token renewToken( ) + TokenResponse refreshToken( RefreshTokenRequest refreshTokenRequest ) 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 * @since 1.4 */ - @Path( "isAuthenticated" ) + @Path( "authenticated" ) @GET @Produces( { MediaType.APPLICATION_JSON } ) @RedbackAuthorization( noRestriction = true ) - User isLogged() + User getAuthenticatedUser() 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; } diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultLoginService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultLoginService.java index a3f50555..d51d3a36 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultLoginService.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultLoginService.java @@ -126,16 +126,16 @@ public class DefaultLoginService return key.getKey( ); } - public PingResult ping() + public Boolean ping() throws RedbackServiceException { - return new PingResult( true); + return Boolean.TRUE; } - public PingResult pingWithAutz() + public Boolean pingWithAutz() throws RedbackServiceException { - return new PingResult( true ); + return Boolean.TRUE; } public User logIn( LoginRequest loginRequest ) diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultUserService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultUserService.java index 320dc8ff..08ea8af0 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultUserService.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/DefaultUserService.java @@ -490,10 +490,10 @@ public class DefaultUserService } @Override - public PingResult ping() + public Boolean ping() throws RedbackServiceException { - return new PingResult( true ); + return Boolean.TRUE; } private User getSimpleUser( org.apache.archiva.redback.users.User user ) diff --git a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java index c7c2af3d..d9ea2565 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/main/java/org/apache/archiva/redback/rest/services/v2/DefaultAuthenticationService.java @@ -23,20 +23,18 @@ import org.apache.archiva.redback.authentication.AuthenticationConstants; import org.apache.archiva.redback.authentication.AuthenticationException; import org.apache.archiva.redback.authentication.AuthenticationFailureCause; 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.TokenAuthenticationException; 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.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.LoginRequest; 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.UserLogin; import org.apache.archiva.redback.rest.api.services.RedbackServiceException; @@ -53,17 +51,11 @@ import javax.inject.Inject; import javax.inject.Named; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; -import java.time.Duration; -import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; -import java.util.Date; 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 public PingResult ping() { @@ -148,15 +109,11 @@ public class DefaultAuthenticationService 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 - public Token logIn( LoginRequest loginRequest ) + public TokenResponse logIn( RequestTokenRequest loginRequest ) throws RedbackServiceException { - String userName = loginRequest.getUsername(), password = loginRequest.getPassword(); + String userName = loginRequest.getUserId(), password = loginRequest.getPassword(); PasswordBasedAuthenticationDataSource authDataSource = new PasswordBasedAuthenticationDataSource( userName, password ); log.debug("Login for {}",userName); @@ -177,8 +134,10 @@ public class DefaultAuthenticationService } // Stateless services no session // httpAuthenticator.authenticate( authDataSource, httpServletRequest.getSession( true ) ); - Token restToken = getRestToken( token ); - return restToken; + org.apache.archiva.redback.authentication.Token refreshToken = jwtAuthenticator.generateToken( user.getUsername( ), TokenType.REFRESH_TOKEN ); + response.setHeader( "Cache-Control", "no-store" ); + response.setHeader( "Pragma", "no-cache" ); + return new TokenResponse(token, refreshToken, "", loginRequest.getState()); } else if ( securitySession.getAuthenticationResult() != null && securitySession.getAuthenticationResult().getAuthenticationFailureCauses() != null ) { @@ -231,13 +190,25 @@ public class DefaultAuthenticationService } @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 - public User isLogged() + public User getAuthenticatedUser() throws RedbackServiceException { SecuritySession securitySession = httpAuthenticator.getSecuritySession( httpServletRequest.getSession( true ) ); @@ -246,23 +217,6 @@ public class DefaultAuthenticationService 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 ) { UserLogin restUser = new UserLogin(); diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/LoginServiceTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/LoginServiceTest.java index e1fc37de..11d73d99 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/LoginServiceTest.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/LoginServiceTest.java @@ -44,6 +44,13 @@ public class LoginServiceTest FakeCreateAdminService.ADMIN_TEST_PWD ) ) ); } + @Test + public void ping() + throws Exception + { + assertNotNull( getLoginService( null ).ping( ) ); + } + @Test public void createUserThenLog() throws Exception diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/UserServiceTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/UserServiceTest.java index 555004eb..ba5a78e1 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/UserServiceTest.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/UserServiceTest.java @@ -57,7 +57,7 @@ public class UserServiceTest public void ping() throws Exception { - Boolean res = getUserService().ping().isSuccess(); + Boolean res = getUserService().ping(); assertTrue( res.booleanValue() ); } diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AbstractNativeRestServices.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AbstractNativeRestServices.java new file mode 100644 index 00000000..2c24730d --- /dev/null +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AbstractNativeRestServices.java @@ -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 + */ +@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 = new AtomicReference<>(); + private static AtomicReference 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(); + } +} diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AbstractRestServicesTestV2.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AbstractRestServicesTestV2.java index ccc9abe8..a1419e12 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AbstractRestServicesTestV2.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AbstractRestServicesTestV2.java @@ -22,10 +22,10 @@ package org.apache.archiva.redback.rest.services.v2; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 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.jwt.JwtAuthenticator; 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.services.FakeCreateAdminService; import org.apache.archiva.redback.rest.services.FakeCreateAdminServiceImpl; @@ -56,6 +56,7 @@ import javax.naming.NameNotFoundException; import javax.naming.NamingException; import javax.naming.directory.DirContext; import javax.ws.rs.core.MediaType; +import java.text.SimpleDateFormat; import java.util.Collections; import java.util.concurrent.atomic.AtomicReference; @@ -137,6 +138,8 @@ public abstract class AbstractRestServicesTestV2 JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider( ); ObjectMapper mapper = new ObjectMapper( ); 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 ); return provider; } @@ -227,7 +230,7 @@ public abstract class AbstractRestServicesTestV2 protected String getRestServicesPath() { - return "restServices"; + return "api"; } public void startServer() diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AuthenticationServiceTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AuthenticationServiceTest.java index 9069a032..b507fc6c 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AuthenticationServiceTest.java +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/AuthenticationServiceTest.java @@ -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.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.TokenResponse; import org.apache.archiva.redback.rest.api.services.RedbackServiceException; import org.apache.archiva.redback.rest.api.services.UserService; import org.apache.archiva.redback.rest.services.FakeCreateAdminService; @@ -64,7 +66,7 @@ public class AuthenticationServiceTest public void loginAdmin() 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 ) ) ); } @@ -117,8 +119,8 @@ public class AuthenticationServiceTest user.setPasswordChangeRequired( false ); um.updateUser( user ); // END SNIPPET: create-user - LoginRequest request = new LoginRequest( "toto", "foo123" ); - Token result = getLoginServiceV2( "" ).logIn( request ); + RequestTokenRequest request = new RequestTokenRequest( "toto", "foo123" ); + TokenResponse result = getLoginServiceV2( "" ).logIn( request ); // assertNotNull( result ); // assertEquals( "toto", result.getUsername( ) ); diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeAuthenticationServiceTest.java b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeAuthenticationServiceTest.java new file mode 100644 index 00000000..2a5182a7 --- /dev/null +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/java/org/apache/archiva/redback/rest/services/v2/NativeAuthenticationServiceTest.java @@ -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 + */ +@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 ) ); + } + +} diff --git a/redback-integrations/redback-rest/redback-rest-services/src/test/resources/log4j2-test.xml b/redback-integrations/redback-rest/redback-rest-services/src/test/resources/log4j2-test.xml index 5f8bb09f..e058bdb7 100644 --- a/redback-integrations/redback-rest/redback-rest-services/src/test/resources/log4j2-test.xml +++ b/redback-integrations/redback-rest/redback-rest-services/src/test/resources/log4j2-test.xml @@ -31,7 +31,7 @@ - +