blob: 0b1df0a4e2fbe4c35d0054236ad835356bfc02c7 [file] [log] [blame]
/*
* 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.
*/
package org.apache.shiro.mgt;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.RememberMeAuthenticationToken;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.crypto.CipherService;
import org.apache.shiro.io.DefaultSerializer;
import org.apache.shiro.io.Serializer;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract implementation of the {@code RememberMeManager} interface that handles
* {@link #setSerializer(org.apache.shiro.io.Serializer) serialization} and
* {@link #setCipherService encryption} of the remembered user identity.
* <p/>
* The remembered identity storage location and details are left to subclasses.
* <h2>Default encryption key</h2>
* This implementation uses an {@link AesCipherService AesCipherService} for strong encryption by default. It also
* uses a default generated symmetric key to both encrypt and decrypt data. As AES is a symmetric cipher, the same
* {@code key} is used to both encrypt and decrypt data, BUT NOTE:
* <p/>
* Because Shiro is an open-source project, if anyone knew that you were using Shiro's default
* {@code key}, they could download/view the source, and with enough effort, reconstruct the {@code key}
* and decode encrypted data at will.
* <p/>
* Of course, this key is only really used to encrypt the remembered {@code PrincipalCollection} which is typically
* a user id or username. So if you do not consider that sensitive information, and you think the default key still
* makes things 'sufficiently difficult', then you can ignore this issue.
* <p/>
* However, if you do feel this constitutes sensitive information, it is recommended that you provide your own
* {@code key} via the {@link #setCipherKey setCipherKey} method to a key known only to your application,
* guaranteeing that no third party can decrypt your data. You can generate your own key by calling the
* {@code CipherService}'s {@link org.apache.shiro.crypto.AesCipherService#generateNewKey() generateNewKey} method
* and using that result as the {@link #setCipherKey cipherKey} configuration attribute.
*
* @since 0.9
*/
public abstract class AbstractRememberMeManager implements RememberMeManager {
/**
* private inner log instance.
*/
private static final Logger log = LoggerFactory.getLogger(AbstractRememberMeManager.class);
/**
* The following Base64 string was generated by auto-generating an AES Key:
* <pre>
* AesCipherService aes = new AesCipherService();
* byte[] key = aes.generateNewKey().getEncoded();
* String base64 = Base64.encodeToString(key);
* </pre>
* The value of 'base64' was copied-n-pasted here:
*/
private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");
/**
* Serializer to use for converting PrincipalCollection instances to/from byte arrays
*/
private Serializer<PrincipalCollection> serializer;
/**
* Cipher to use for encrypting/decrypting serialized byte arrays for added security
*/
private CipherService cipherService;
/**
* Cipher encryption key to use with the Cipher when encrypting data
*/
private byte[] encryptionCipherKey;
/**
* Cipher decryption key to use with the Cipher when decrypting data
*/
private byte[] decryptionCipherKey;
/**
* Default constructor that initializes a {@link DefaultSerializer} as the {@link #getSerializer() serializer} and
* an {@link AesCipherService} as the {@link #getCipherService() cipherService}.
*/
public AbstractRememberMeManager() {
this.serializer = new DefaultSerializer<PrincipalCollection>();
this.cipherService = new AesCipherService();
setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
}
/**
* Returns the {@code Serializer} used to serialize and deserialize {@link PrincipalCollection} instances for
* persistent remember me storage.
* <p/>
* Unless overridden by the {@link #setSerializer} method, the default instance is a
* {@link org.apache.shiro.io.DefaultSerializer}.
*
* @return the {@code Serializer} used to serialize and deserialize {@link PrincipalCollection} instances for
* persistent remember me storage.
*/
public Serializer<PrincipalCollection> getSerializer() {
return serializer;
}
/**
* Sets the {@code Serializer} used to serialize and deserialize {@link PrincipalCollection} instances for
* persistent remember me storage.
* <p/>
* Unless overridden by this method, the default instance is a {@link DefaultSerializer}.
*
* @param serializer the {@code Serializer} used to serialize and deserialize {@link PrincipalCollection} instances
* for persistent remember me storage.
*/
public void setSerializer(Serializer<PrincipalCollection> serializer) {
this.serializer = serializer;
}
/**
* Returns the {@code CipherService} to use for encrypting and decrypting serialized identity data to prevent easy
* inspection of Subject identity data.
* <p/>
* Unless overridden by the {@link #setCipherService} method, the default instance is an {@link AesCipherService}.
*
* @return the {@code Cipher} to use for encrypting and decrypting serialized identity data to prevent easy
* inspection of Subject identity data
*/
public CipherService getCipherService() {
return cipherService;
}
/**
* Sets the {@code CipherService} to use for encrypting and decrypting serialized identity data to prevent easy
* inspection of Subject identity data.
* <p/>
* If the CipherService is a symmetric CipherService (using the same key for both encryption and decryption), you
* should set your key via the {@link #setCipherKey(byte[])} method.
* <p/>
* If the CipherService is an asymmetric CipherService (different keys for encryption and decryption, such as
* public/private key pairs), you should set your encryption and decryption key via the respective
* {@link #setEncryptionCipherKey(byte[])} and {@link #setDecryptionCipherKey(byte[])} methods.
* <p/>
* <b>N.B.</b> Unless overridden by this method, the default CipherService instance is an
* {@link AesCipherService}. This {@code RememberMeManager} implementation already has a configured symmetric key
* to use for encryption and decryption, but it is recommended to provide your own for added security. See the
* class-level JavaDoc for more information and why it might be good to provide your own.
*
* @param cipherService the {@code CipherService} to use for encrypting and decrypting serialized identity data to
* prevent easy inspection of Subject identity data.
*/
public void setCipherService(CipherService cipherService) {
this.cipherService = cipherService;
}
/**
* Returns the cipher key to use for encryption operations.
*
* @return the cipher key to use for encryption operations.
* @see #setCipherService for a description of the various {@code get/set*Key} methods.
*/
public byte[] getEncryptionCipherKey() {
return encryptionCipherKey;
}
/**
* Sets the encryption key to use for encryption operations.
*
* @param encryptionCipherKey the encryption key to use for encryption operations.
* @see #setCipherService for a description of the various {@code get/set*Key} methods.
*/
public void setEncryptionCipherKey(byte[] encryptionCipherKey) {
this.encryptionCipherKey = encryptionCipherKey;
}
/**
* Returns the decryption cipher key to use for decryption operations.
*
* @return the cipher key to use for decryption operations.
* @see #setCipherService for a description of the various {@code get/set*Key} methods.
*/
public byte[] getDecryptionCipherKey() {
return decryptionCipherKey;
}
/**
* Sets the decryption key to use for decryption operations.
*
* @param decryptionCipherKey the decryption key to use for decryption operations.
* @see #setCipherService for a description of the various {@code get/set*Key} methods.
*/
public void setDecryptionCipherKey(byte[] decryptionCipherKey) {
this.decryptionCipherKey = decryptionCipherKey;
}
/**
* Convenience method that returns the cipher key to use for <em>both</em> encryption and decryption.
* <p/>
* <b>N.B.</b> This method can only be called if the underlying {@link #getCipherService() cipherService} is a symmetric
* CipherService which by definition uses the same key for both encryption and decryption. If using an asymmetric
* CipherService public/private key pair, you cannot use this method, and should instead use the
* {@link #getEncryptionCipherKey()} and {@link #getDecryptionCipherKey()} methods individually.
* <p/>
* The default {@link AesCipherService} instance is a symmetric cipher service, so this method can be used if you are
* using the default.
*
* @return the symmetric cipher key used for both encryption and decryption.
*/
public byte[] getCipherKey() {
//Since this method should only be used with symmetric ciphers
//(where the enc and dec keys are the same), either is fine, just return one of them:
return getEncryptionCipherKey();
}
/**
* Convenience method that sets the cipher key to use for <em>both</em> encryption and decryption.
* <p/>
* <b>N.B.</b> This method can only be called if the underlying {@link #getCipherService() cipherService} is a
* symmetric CipherService?which by definition uses the same key for both encryption and decryption. If using an
* asymmetric CipherService?(such as a public/private key pair), you cannot use this method, and should instead use
* the {@link #setEncryptionCipherKey(byte[])} and {@link #setDecryptionCipherKey(byte[])} methods individually.
* <p/>
* The default {@link AesCipherService} instance is a symmetric CipherService, so this method can be used if you
* are using the default.
*
* @param cipherKey the symmetric cipher key to use for both encryption and decryption.
*/
public void setCipherKey(byte[] cipherKey) {
//Since this method should only be used in symmetric ciphers
//(where the enc and dec keys are the same), set it on both:
setEncryptionCipherKey(cipherKey);
setDecryptionCipherKey(cipherKey);
}
/**
* Forgets (removes) any remembered identity data for the specified {@link Subject} instance.
*
* @param subject the subject instance for which identity data should be forgotten from the underlying persistence
* mechanism.
*/
protected abstract void forgetIdentity(Subject subject);
/**
* Determines whether or not remember me services should be performed for the specified token. This method returns
* {@code true} iff:
* <ol>
* <li>The token is not {@code null} and</li>
* <li>The token is an {@code instanceof} {@link RememberMeAuthenticationToken} and</li>
* <li>{@code token}.{@link org.apache.shiro.authc.RememberMeAuthenticationToken#isRememberMe() isRememberMe()} is
* {@code true}</li>
* </ol>
*
* @param token the authentication token submitted during the successful authentication attempt.
* @return true if remember me services should be performed as a result of the successful authentication attempt.
*/
protected boolean isRememberMe(AuthenticationToken token) {
return token != null && (token instanceof RememberMeAuthenticationToken) &&
((RememberMeAuthenticationToken) token).isRememberMe();
}
/**
* Reacts to the successful login attempt by first always {@link #forgetIdentity(Subject) forgetting} any previously
* stored identity. Then if the {@code token}
* {@link #isRememberMe(org.apache.shiro.authc.AuthenticationToken) is a RememberMe} token, the associated identity
* will be {@link #rememberIdentity(org.apache.shiro.subject.Subject, org.apache.shiro.authc.AuthenticationToken, org.apache.shiro.authc.AuthenticationInfo) remembered}
* for later retrieval during a new user session.
*
* @param subject the subject for which the principals are being remembered.
* @param token the token that resulted in a successful authentication attempt.
* @param info the authentication info resulting from the successful authentication attempt.
*/
public void onSuccessfulLogin(Subject subject, AuthenticationToken token, AuthenticationInfo info) {
//always clear any previous identity:
forgetIdentity(subject);
//now save the new identity:
if (isRememberMe(token)) {
rememberIdentity(subject, token, info);
} else {
if (log.isDebugEnabled()) {
log.debug("AuthenticationToken did not indicate RememberMe is requested. " +
"RememberMe functionality will not be executed for corresponding account.");
}
}
}
/**
* Remembers a subject-unique identity for retrieval later. This implementation first
* {@link #getIdentityToRemember resolves} the exact
* {@link PrincipalCollection principals} to remember. It then remembers the principals by calling
* {@link #rememberIdentity(org.apache.shiro.subject.Subject, org.apache.shiro.subject.PrincipalCollection)}.
* <p/>
* This implementation ignores the {@link AuthenticationToken} argument, but it is available to subclasses if
* necessary for custom logic.
*
* @param subject the subject for which the principals are being remembered.
* @param token the token that resulted in a successful authentication attempt.
* @param authcInfo the authentication info resulting from the successful authentication attempt.
*/
public void rememberIdentity(Subject subject, AuthenticationToken token, AuthenticationInfo authcInfo) {
PrincipalCollection principals = getIdentityToRemember(subject, authcInfo);
rememberIdentity(subject, principals);
}
/**
* Returns {@code info}.{@link org.apache.shiro.authc.AuthenticationInfo#getPrincipals() getPrincipals()} and
* ignores the {@link Subject} argument.
*
* @param subject the subject for which the principals are being remembered.
* @param info the authentication info resulting from the successful authentication attempt.
* @return the {@code PrincipalCollection} to remember.
*/
protected PrincipalCollection getIdentityToRemember(Subject subject, AuthenticationInfo info) {
return info.getPrincipals();
}
/**
* Remembers the specified account principals by first
* {@link #convertPrincipalsToBytes(org.apache.shiro.subject.PrincipalCollection) converting} them to a byte
* array and then {@link #rememberSerializedIdentity(org.apache.shiro.subject.Subject, byte[]) remembers} that
* byte array.
*
* @param subject the subject for which the principals are being remembered.
* @param accountPrincipals the principals to remember for retrieval later.
*/
protected void rememberIdentity(Subject subject, PrincipalCollection accountPrincipals) {
byte[] bytes = convertPrincipalsToBytes(accountPrincipals);
rememberSerializedIdentity(subject, bytes);
}
/**
* Converts the given principal collection the byte array that will be persisted to be 'remembered' later.
* <p/>
* This implementation first {@link #serialize(org.apache.shiro.subject.PrincipalCollection) serializes} the
* principals to a byte array and then {@link #encrypt(byte[]) encrypts} that byte array.
*
* @param principals the {@code PrincipalCollection} to convert to a byte array
* @return the representative byte array to be persisted for remember me functionality.
*/
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
byte[] bytes = serialize(principals);
if (getCipherService() != null) {
bytes = encrypt(bytes);
}
return bytes;
}
/**
* Persists the identity bytes to a persistent store for retrieval later via the
* {@link #getRememberedSerializedIdentity(SubjectContext)} method.
*
* @param subject the Subject for which the identity is being serialized.
* @param serialized the serialized bytes to be persisted.
*/
protected abstract void rememberSerializedIdentity(Subject subject, byte[] serialized);
/**
* Implements the interface method by first {@link #getRememberedSerializedIdentity(SubjectContext) acquiring}
* the remembered serialized byte array. Then it {@link #convertBytesToPrincipals(byte[], SubjectContext) converts}
* them and returns the re-constituted {@link PrincipalCollection}. If no remembered principals could be
* obtained, {@code null} is returned.
* <p/>
* If any exceptions are thrown, the {@link #onRememberedPrincipalFailure(RuntimeException, SubjectContext)} method
* is called to allow any necessary post-processing (such as immediately removing any previously remembered
* values for safety).
*
* @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation, that
* is being used to construct a {@link Subject} instance.
* @return the remembered principals or {@code null} if none could be acquired.
*/
public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) {
PrincipalCollection principals = null;
try {
byte[] bytes = getRememberedSerializedIdentity(subjectContext);
//SHIRO-138 - only call convertBytesToPrincipals if bytes exist:
if (bytes != null && bytes.length > 0) {
principals = convertBytesToPrincipals(bytes, subjectContext);
}
} catch (RuntimeException re) {
principals = onRememberedPrincipalFailure(re, subjectContext);
}
return principals;
}
/**
* Based on the given subject context data, retrieves the previously persisted serialized identity, or
* {@code null} if there is no available data. The context map is usually populated by a {@link Subject.Builder}
* implementation. See the {@link SubjectFactory} class constants for Shiro's known map keys.
*
* @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation, that
* is being used to construct a {@link Subject} instance. To be used to assist with data
* lookup.
* @return the previously persisted serialized identity, or {@code null} if there is no available data for the
* Subject.
*/
protected abstract byte[] getRememberedSerializedIdentity(SubjectContext subjectContext);
/**
* If a {@link #getCipherService() cipherService} is available, it will be used to first decrypt the byte array.
* Then the bytes are then {@link #deserialize(byte[]) deserialized} and then returned.
*
* @param bytes the bytes to decrypt if necessary and then deserialize.
* @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation, that
* is being used to construct a {@link Subject} instance.
* @return the de-serialized and possibly decrypted principals
*/
protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) {
if (getCipherService() != null) {
bytes = decrypt(bytes);
}
return deserialize(bytes);
}
/**
* Called when an exception is thrown while trying to retrieve principals. The default implementation logs a
* debug message and forgets ('unremembers') the problem identity by calling
* {@link #forgetIdentity(SubjectContext) forgetIdentity(context)} and then immediately re-throws the
* exception to allow the calling component to react accordingly.
* <p/>
* This method implementation never returns an
* object - it always rethrows, but can be overridden by subclasses for custom handling behavior.
* <p/>
* This most commonly would be called when an encryption key is updated and old principals are retrieved that have
* been encrypted with the previous key.
*
* @param e the exception that was thrown.
* @param context the contextual data, usually provided by a {@link Subject.Builder} implementation, that
* is being used to construct a {@link Subject} instance.
* @return nothing - the original {@code RuntimeException} is propagated in all cases.
*/
protected PrincipalCollection onRememberedPrincipalFailure(RuntimeException e, SubjectContext context) {
if (log.isDebugEnabled()) {
log.debug("There was a failure while trying to retrieve remembered principals. This could be due to a " +
"configuration problem or corrupted principals. This could also be due to a recently " +
"changed encryption key. The remembered identity will be forgotten and not used for this " +
"request.", e);
}
forgetIdentity(context);
//propagate - security manager implementation will handle and warn appropriately
throw e;
}
/**
* Encrypts the byte array by using the configured {@link #getCipherService() cipherService}.
*
* @param serialized the serialized object byte array to be encrypted
* @return an encrypted byte array returned by the configured {@link #getCipherService () cipher}.
*/
protected byte[] encrypt(byte[] serialized) {
byte[] value = serialized;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.encrypt(serialized, getEncryptionCipherKey());
value = byteSource.getBytes();
}
return value;
}
/**
* Decrypts the byte array using the configured {@link #getCipherService() cipherService}.
*
* @param encrypted the encrypted byte array to decrypt
* @return the decrypted byte array returned by the configured {@link #getCipherService () cipher}.
*/
protected byte[] decrypt(byte[] encrypted) {
byte[] serialized = encrypted;
CipherService cipherService = getCipherService();
if (cipherService != null) {
ByteSource byteSource = cipherService.decrypt(encrypted, getDecryptionCipherKey());
serialized = byteSource.getBytes();
}
return serialized;
}
/**
* Serializes the given {@code principals} by serializing them to a byte array by using the
* {@link #getSerializer() serializer}'s {@link Serializer#serialize(Object) serialize} method.
*
* @param principals the principal collection to serialize to a byte array
* @return the serialized principal collection in the form of a byte array
*/
protected byte[] serialize(PrincipalCollection principals) {
return getSerializer().serialize(principals);
}
/**
* De-serializes the given byte array by using the {@link #getSerializer() serializer}'s
* {@link Serializer#deserialize deserialize} method.
*
* @param serializedIdentity the previously serialized {@code PrincipalCollection} as a byte array
* @return the de-serialized (reconstituted) {@code PrincipalCollection}
*/
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
return getSerializer().deserialize(serializedIdentity);
}
/**
* Reacts to a failed login by immediately {@link #forgetIdentity(org.apache.shiro.subject.Subject) forgetting} any
* previously remembered identity. This is an additional security feature to prevent any remenant identity data
* from being retained in case the authentication attempt is not being executed by the expected user.
*
* @param subject the subject which executed the failed login attempt
* @param token the authentication token resulting in a failed login attempt - ignored by this implementation
* @param ae the exception thrown as a result of the failed login attempt - ignored by this implementation
*/
public void onFailedLogin(Subject subject, AuthenticationToken token, AuthenticationException ae) {
forgetIdentity(subject);
}
/**
* Reacts to a subject logging out of the application and immediately
* {@link #forgetIdentity(org.apache.shiro.subject.Subject) forgets} any previously stored identity and returns.
*
* @param subject the subject logging out.
*/
public void onLogout(Subject subject) {
forgetIdentity(subject);
}
}