From de5816adf044d94245bb9e97249b2d27e6507bce Mon Sep 17 00:00:00 2001 From: Martin Stockhammer Date: Sat, 11 Feb 2017 20:20:24 +0100 Subject: [PATCH] Adding validation token generator got login --- .../EncryptionFailedException.java | 47 ++++ .../authentication/InvalidTokenException.java | 48 ++++ .../authentication/SimpleTokenData.java | 88 +++++++ .../redback/authentication/TokenData.java | 66 +++++ .../redback/authentication/TokenManager.java | 248 ++++++++++++++++++ .../resources/META-INF/spring-context.xml | 4 +- .../authentication/TokenManagerTest.java | 153 +++++++++++ .../archiva/redback/rest/api/model/User.java | 16 ++ .../rest/services/DefaultLoginService.java | 12 + .../redback/system/DefaultSecuritySystem.java | 15 ++ .../redback/system/SecuritySystem.java | 8 + 11 files changed, 704 insertions(+), 1 deletion(-) create mode 100644 redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/EncryptionFailedException.java create mode 100644 redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/InvalidTokenException.java create mode 100644 redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java create mode 100644 redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenData.java create mode 100644 redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/TokenManager.java create mode 100644 redback-authentication/redback-authentication-api/src/test/java/org/apache/archiva/redback/authentication/TokenManagerTest.java diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/EncryptionFailedException.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/EncryptionFailedException.java new file mode 100644 index 00000000..83c1079a --- /dev/null +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/EncryptionFailedException.java @@ -0,0 +1,47 @@ +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. + */ + +/** + * + * Exception used by the token manager. + * + * Created by Martin Stockhammer on 11.02.17. + */ +public class EncryptionFailedException extends Exception { + + private static final long serialVersionUID = -2275802156651048276L; + + public EncryptionFailedException() { + super(); + } + + public EncryptionFailedException(String message) { + super(message); + } + + public EncryptionFailedException(String message, Throwable cause) { + super(message, cause); + } + + public EncryptionFailedException(Throwable cause) { + super(cause); + } +} diff --git a/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/InvalidTokenException.java b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/InvalidTokenException.java new file mode 100644 index 00000000..b3af182c --- /dev/null +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/InvalidTokenException.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. + */ + +/** + * Exception used by the token manager. + * + * Created by Martin Stockhammer on 11.02.17. + */ +public class InvalidTokenException extends Exception { + + private static final long serialVersionUID = -1148088610607667870L; + + public InvalidTokenException() { + super(); + } + + public InvalidTokenException(String message) { + super(message); + } + + public InvalidTokenException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidTokenException(Throwable cause) { + super(cause); + } + + +} 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 new file mode 100644 index 00000000..e3302bfc --- /dev/null +++ b/redback-authentication/redback-authentication-api/src/main/java/org/apache/archiva/redback/authentication/SimpleTokenData.java @@ -0,0 +1,88 @@ +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. + */ + +import java.io.Serializable; +import java.util.Date; + +/** + * + * Simple Token information class that contains a username and a lifetime. + * + * The class is not able to detect time manipulations. It is assumed that the + * current time of the system is correct. + * + * This class is immutable. + * + * Created by Martin Stockhammer on 03.02.17. + */ +public final class SimpleTokenData implements Serializable, TokenData { + + + private static final long serialVersionUID = 5907745449771921813L; + + private final String user; + private final Date created; + private final Date validBefore; + private final long 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 long lifetime, final long nonce) { + this.user=user; + this.created=new Date(); + this.validBefore =new Date(created.getTime()+lifetime); + this.nonce = nonce; + } + + @Override + public final String getUser() { + return user; + } + + @Override + public final Date created() { + return created; + } + + @Override + public final Date validBefore() { + return validBefore; + } + + @Override + public final long getNonce() { + return nonce; + } + + @Override + public boolean isValid() { + return (new Date().getTime())0) { + keyGen.init(keySize); + } + if (keyAlg.length==3 && keyAlg[2].equals("NoPadding")) { + paddingUsed=false; + } + SecretKey secretKey = keyGen.generateKey(); + enCipher.init(Cipher.ENCRYPT_MODE, secretKey); + // We have to provide the IV depending on the algorithm used + // CBC needs an IV, ECB not. + if (enCipher.getIV()==null) { + deCipher.init(Cipher.DECRYPT_MODE, secretKey); + } else { + deCipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(enCipher.getIV())); + } + } catch (NoSuchAlgorithmException e) { + log.error("Error occurred during key initialization. Requested algorithm not available. "+e.getMessage()); + throw e; + } catch (NoSuchPaddingException e) { + log.error("Error occurred during key initialization. Requested padding not available. "+e.getMessage()); + throw e; + } catch (InvalidKeyException e) { + log.error("The key is not valid."); + throw e; + } catch (InvalidAlgorithmParameterException e) { + log.error("Invalid encryption parameters."); + throw e; + } + } + + + @SuppressWarnings("SameParameterValue") + public String encryptToken(String user, long lifetime) throws EncryptionFailedException { + return encryptToken(new SimpleTokenData(user, lifetime, createNonce())); + } + + public String encryptToken(TokenData tokenData) throws EncryptionFailedException { + try { + return encode(encrypt(tokenData)); + } catch (IOException e) { + log.error("Error during object conversion: "+e.getMessage()); + throw new EncryptionFailedException(e); + } catch (BadPaddingException e) { + log.error("Padding invalid"); + throw new EncryptionFailedException(e); + } catch (IllegalBlockSizeException e) { + log.error("Block size invalid"); + throw new EncryptionFailedException(e); + } + } + + public TokenData decryptToken(String token) throws InvalidTokenException { + try { + return decrypt(decode(token)); + } catch (IOException ex) { + log.error("Error during data read. " + ex.getMessage()); + throw new InvalidTokenException(ex); + } catch (ClassNotFoundException ex) { + log.error("Token data invalid."); + throw new InvalidTokenException(ex); + } catch (BadPaddingException ex) { + log.error("The encrypted token has the wrong padding."); + throw new InvalidTokenException(ex); + } catch (IllegalBlockSizeException ex) { + log.error("The encrypted token has the wrong block size."); + throw new InvalidTokenException(ex); + } + } + + private long createNonce() { + return rd.nextLong(); + } + + protected byte[] encrypt(TokenData info) throws IOException, BadPaddingException, IllegalBlockSizeException { + return doEncrypt(convertToByteArray(info)); + } + + protected byte[] doEncrypt(byte[] data) throws BadPaddingException, IllegalBlockSizeException { + byte[] encData; + if (!paddingUsed && (data.length % enCipher.getBlockSize())!=0) { + int blocks = data.length / enCipher.getBlockSize(); + encData = Arrays.copyOf(data, enCipher.getBlockSize()*(blocks+1)); + } else { + encData = data; + } + return enCipher.doFinal(encData); + } + + protected TokenData decrypt(byte[] token) throws BadPaddingException, IllegalBlockSizeException, IOException, ClassNotFoundException { + Object result = convertFromByteArray(doDecrypt(token)); + if (!(result instanceof TokenData)) { + throw new InvalidClassException("No TokenData found in decrypted token"); + } + return (TokenData)result; + } + + protected byte[] doDecrypt(byte[] encryptedData) throws BadPaddingException, IllegalBlockSizeException { + return deCipher.doFinal(encryptedData); + } + + private String encode(byte[] token) { + return Base64.encodeBase64String(token); + } + + private byte[] decode(String token) { + return Base64.decodeBase64(token); + } + + + private Object convertFromByteArray(byte[] byteObject) throws IOException, + ClassNotFoundException { + ByteArrayInputStream bais; + ObjectInputStream in; + bais = new ByteArrayInputStream(byteObject); + in = new ObjectInputStream(bais); + Object o = in.readObject(); + in.close(); + return o; + + } + + + private byte[] convertToByteArray(Object complexObject) throws IOException { + ByteArrayOutputStream baos; + ObjectOutputStream out; + baos = new ByteArrayOutputStream(); + out = new ObjectOutputStream(baos); + out.writeObject(complexObject); + out.close(); + return baos.toByteArray(); + } + + public String getAlgorithm() { + return algorithm; + } + + /** + * Sets the encryption algorithm and resets the key size. You may change the key size after + * calling this method. + * Additionally run the initialize() method after setting algorithm and keysize. + * + * + * @param algorithm The encryption algorithm to use. + */ + public void setAlgorithm(String algorithm) { + if (!this.algorithm.equals(algorithm)) { + this.algorithm = algorithm; + this.keySize=-1; + } + } + + public int getKeySize() { + return keySize; + } + + /** + * Sets the key size for the encryption. This method must be called after + * setting the algorithm. The keysize will be reset by calling setAlgorithm() + * + * The key size must be valid for the given algorithm. + * + * @param keySize The size of the encryption key + */ + public void setKeySize(int keySize) { + this.keySize = keySize; + } +} \ No newline at end of file diff --git a/redback-authentication/redback-authentication-api/src/main/resources/META-INF/spring-context.xml b/redback-authentication/redback-authentication-api/src/main/resources/META-INF/spring-context.xml index a07857cb..d2aab376 100644 --- a/redback-authentication/redback-authentication-api/src/main/resources/META-INF/spring-context.xml +++ b/redback-authentication/redback-authentication-api/src/main/resources/META-INF/spring-context.xml @@ -30,5 +30,7 @@ - + + + \ No newline at end of file diff --git a/redback-authentication/redback-authentication-api/src/test/java/org/apache/archiva/redback/authentication/TokenManagerTest.java b/redback-authentication/redback-authentication-api/src/test/java/org/apache/archiva/redback/authentication/TokenManagerTest.java new file mode 100644 index 00000000..5200956c --- /dev/null +++ b/redback-authentication/redback-authentication-api/src/test/java/org/apache/archiva/redback/authentication/TokenManagerTest.java @@ -0,0 +1,153 @@ +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. + */ + +import org.junit.Test; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import static org.junit.Assert.*; + +/** + * Test the TokenManager implementation. Uses no spring dependencies. + * + * Created by Martin Stockhammer on 11.02.17. + */ +public class TokenManagerTest { + + @Test + public void initialize() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, EncryptionFailedException, InvalidAlgorithmParameterException { + TokenManager tokenManager = new TokenManager(); + tokenManager.initialize(); + } + + @Test + public void encryptToken() throws Exception { + TokenManager tokenManager = new TokenManager(); + tokenManager.initialize(); + assertEquals(tokenManager.getAlgorithm(),"AES/ECB/PKCS5Padding"); + assertEquals(tokenManager.getKeySize(), -1); + String token = tokenManager.encryptToken("testuser01",1000); + assertNotNull(token); + assertTrue("Token size too low",token.length()>300); + + } + + @Test + public void decryptToken() throws Exception { + TokenManager tokenManager = new TokenManager(); + tokenManager.initialize(); + String token = tokenManager.encryptToken("testuser00003",1000); + assertNotNull(token); + assertTrue("Token size too low",token.length()>300); + TokenData tokenData = tokenManager.decryptToken(token); + assertNotNull(tokenData); + assertEquals("testuser00003", tokenData.getUser()); + assertTrue(tokenData.isValid()); + + } + + @Test + public void decryptExpiredToken() throws Exception { + TokenManager tokenManager = new TokenManager(); + tokenManager.initialize(); + SimpleTokenData sToken = new SimpleTokenData("testuser00003", 0, 1345455); + String token = tokenManager.encryptToken(sToken); + assertNotNull(token); + assertTrue("Token size too low",token.length()>300); + TokenData tokenData = tokenManager.decryptToken(token); + assertNotNull(tokenData); + assertEquals("testuser00003", tokenData.getUser()); + assertFalse(tokenData.isValid()); + + } + + @Test(expected = InvalidTokenException.class) + public void decryptInvalidToken() throws Exception { + TokenManager tokenManager = new TokenManager(); + tokenManager.initialize(); + SimpleTokenData sToken = new SimpleTokenData("testuser00003", 0, 1345455); + String token = tokenManager.encryptToken(sToken); + assertNotNull(token); + assertTrue("Token size too low",token.length()>300); + tokenManager.initialize(); + tokenManager.decryptToken(token); + + } + + @Test + public void decryptTokenWithDifferentAlgorithm() throws Exception { + TokenManager tokenManager = new TokenManager(); + tokenManager.setAlgorithm("DES/ECB/PKCS5Padding"); + tokenManager.initialize(); + String token = tokenManager.encryptToken("testuser00005",2000); + assertNotNull(token); + assertTrue("Token size too low",token.length()>300); + TokenData tokenData = tokenManager.decryptToken(token); + assertNotNull(tokenData); + assertEquals("testuser00005", tokenData.getUser()); + assertTrue(tokenData.isValid()); + + + tokenManager.setAlgorithm("DES/ECB/NoPadding"); + tokenManager.initialize(); + token = tokenManager.encryptToken("testuser00006",2000); + assertNotNull(token); + assertTrue("Token size too low",token.length()>300); + tokenData = tokenManager.decryptToken(token); + assertNotNull(tokenData); + assertEquals("testuser00006", tokenData.getUser()); + assertTrue(tokenData.isValid()); + + } + + @Test + public void nativeEncryption() throws EncryptionFailedException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidTokenException, BadPaddingException, IllegalBlockSizeException { + TokenManager tokenManager = new TokenManager(); + tokenManager.setAlgorithm("DES/CBC/PKCS5Padding"); + tokenManager.setKeySize(56); + tokenManager.initialize(); + byte[] data = { 1, 5, 12, 18, 124, 44, 88, -28, -44}; + byte[] token = tokenManager.doEncrypt(data); + assertNotNull(token); + byte[] tokenData = tokenManager.doDecrypt(token); + assertNotNull(tokenData); + assertArrayEquals(data, tokenData); + + + tokenManager.setAlgorithm("AES/CBC/NoPadding"); + tokenManager.setKeySize(128); + tokenManager.initialize(); + token = tokenManager.doEncrypt(data); + assertNotNull(token); + // Without padding the decrypted value is a multiple of the block size. + tokenData = Arrays.copyOf(tokenManager.doDecrypt(token), data.length); + assertNotNull(tokenData); + assertArrayEquals(data, tokenData); + + } + +} \ No newline at end of file diff --git a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/User.java b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/User.java index 1b6d50cf..2f3e93e6 100644 --- a/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/User.java +++ b/redback-integrations/redback-rest/redback-rest-api/src/main/java/org/apache/archiva/redback/rest/api/model/User.java @@ -80,6 +80,14 @@ public class User */ private String userManagerId; + /** + * for request validation + * + * @since 2.2 + */ + private String validationToken; + + public User() { // no op @@ -272,6 +280,14 @@ public class User this.userManagerId = userManagerId; } + public String getValidationToken() { + return validationToken; + } + + public void setValidationToken(String validationToken) { + this.validationToken = validationToken; + } + @Override public String toString() { 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 ee3cc478..3bc30bb3 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 @@ -22,7 +22,9 @@ package org.apache.archiva.redback.rest.services; 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.EncryptionFailedException; import org.apache.archiva.redback.authentication.PasswordBasedAuthenticationDataSource; +import org.apache.archiva.redback.authentication.TokenManager; import org.apache.archiva.redback.integration.filter.authentication.HttpAuthenticator; import org.apache.archiva.redback.keys.AuthenticationKey; import org.apache.archiva.redback.keys.KeyManager; @@ -75,6 +77,9 @@ public class DefaultLoginService @Context private HttpServletRequest httpServletRequest; + // validation token lifetime: 3 hours + long tokenLifetime = 1000*3600*3; + @Inject public DefaultLoginService( SecuritySystem securitySystem, @Named( "httpAuthenticator#basic" ) HttpAuthenticator httpAuthenticator ) @@ -149,6 +154,13 @@ public class DefaultLoginService } User restUser = buildRestUser( user ); restUser.setReadOnly( securitySystem.userManagerReadOnly() ); + // validationToken only set during login + try { + restUser.setValidationToken(securitySystem.getTokenManager().encryptToken(user.getUsername(), tokenLifetime)); + } catch (EncryptionFailedException e) { + log.error("Validation token could not be created "+e.getMessage()); + } + // here create an http session httpAuthenticator.authenticate( authDataSource, httpServletRequest.getSession( true ) ); return restUser; diff --git a/redback-system/src/main/java/org/apache/archiva/redback/system/DefaultSecuritySystem.java b/redback-system/src/main/java/org/apache/archiva/redback/system/DefaultSecuritySystem.java index 2f90c792..811903e8 100644 --- a/redback-system/src/main/java/org/apache/archiva/redback/system/DefaultSecuritySystem.java +++ b/redback-system/src/main/java/org/apache/archiva/redback/system/DefaultSecuritySystem.java @@ -19,10 +19,12 @@ package org.apache.archiva.redback.system; * under the License. */ +import com.sun.javafx.fxml.expression.Expression; import org.apache.archiva.redback.authentication.AuthenticationDataSource; import org.apache.archiva.redback.authentication.AuthenticationException; import org.apache.archiva.redback.authentication.AuthenticationManager; import org.apache.archiva.redback.authentication.AuthenticationResult; +import org.apache.archiva.redback.authentication.TokenManager; import org.apache.archiva.redback.authorization.AuthorizationDataSource; import org.apache.archiva.redback.authorization.AuthorizationException; import org.apache.archiva.redback.authorization.AuthorizationResult; @@ -68,6 +70,10 @@ public class DefaultSecuritySystem @Named( value = "keyManager#cached" ) private KeyManager keyManager; + @Inject + @Named( value = "tokenManager#default") + private TokenManager tokenManager; + @Inject private UserSecurityPolicy policy; @@ -305,4 +311,13 @@ public class DefaultSecuritySystem return userManager.isReadOnly(); } + @Override + public TokenManager getTokenManager() { + return tokenManager; + } + + public void setTokenManager(TokenManager tokenManager) { + this.tokenManager = tokenManager; + } + } diff --git a/redback-system/src/main/java/org/apache/archiva/redback/system/SecuritySystem.java b/redback-system/src/main/java/org/apache/archiva/redback/system/SecuritySystem.java index c7b049d4..f12868a2 100644 --- a/redback-system/src/main/java/org/apache/archiva/redback/system/SecuritySystem.java +++ b/redback-system/src/main/java/org/apache/archiva/redback/system/SecuritySystem.java @@ -19,6 +19,7 @@ package org.apache.archiva.redback.system; * under the License. */ +import org.apache.archiva.redback.authentication.TokenManager; import org.apache.archiva.redback.policy.AccountLockedException; import org.apache.archiva.redback.policy.MustChangePasswordException; import org.apache.archiva.redback.policy.UserSecurityPolicy; @@ -111,5 +112,12 @@ public interface SecuritySystem * @since 2.1 */ boolean userManagerReadOnly(); + + /** + * Returns the token manager implementation. + * + * @since 2.2 + */ + TokenManager getTokenManager(); }