org.springframework.security.core.token.KeyBasedPersistenceTokenService Maven / Gradle / Ivy
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.security.core.token;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Date;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Basic implementation of {@link TokenService} that is compatible with clusters and
* across machine restarts, without requiring database persistence.
*
*
* Keys are produced in the format:
*
*
*
* Base64(creationTime + ":" + hex(pseudoRandomNumber) + ":" + extendedInformation + ":" +
* Sha512Hex(creationTime + ":" + hex(pseudoRandomNumber) + ":" + extendedInformation +
* ":" + serverSecret) )
*
*
*
* In the above, creationTime
, tokenKey
and
* extendedInformation
are equal to that stored in {@link Token}. The
* Sha512Hex
includes the same payload, plus a serverSecret
.
*
*
*
* The serverSecret
varies every millisecond. It relies on two static
* server-side secrets. The first is a password, and the second is a server integer. Both
* of these must remain the same for any issued keys to subsequently be recognised. The
* applicable serverSecret
in any millisecond is computed by
* password
+ ":" + (creationTime
% serverInteger
).
* This approach further obfuscates the actual server secret and renders attempts to
* compute the server secret more limited in usefulness (as any false tokens would be
* forced to have a creationTime
equal to the computed hash). Recall that
* framework features depending on token services should reject tokens that are relatively
* old in any event.
*
*
*
* A further consideration of this class is the requirement for cryptographically strong
* pseudo-random numbers. To this end, the use of {@link SecureRandomFactoryBean} is
* recommended to inject the property.
*
*
*
* This implementation uses UTF-8 encoding internally for string manipulation.
*
*
* @author Ben Alex
*
*/
public class KeyBasedPersistenceTokenService implements TokenService, InitializingBean {
private int pseudoRandomNumberBytes = 32;
private String serverSecret;
private Integer serverInteger;
private SecureRandom secureRandom;
@Override
public Token allocateToken(String extendedInformation) {
Assert.notNull(extendedInformation, "Must provided non-null extendedInformation (but it can be empty)");
long creationTime = new Date().getTime();
String serverSecret = computeServerSecretApplicableAt(creationTime);
String pseudoRandomNumber = generatePseudoRandomNumber();
String content = creationTime + ":" + pseudoRandomNumber + ":" + extendedInformation;
String key = computeKey(serverSecret, content);
return new DefaultToken(key, creationTime, extendedInformation);
}
private String computeKey(String serverSecret, String content) {
String sha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret);
String keyPayload = content + ":" + sha512Hex;
return Utf8.decode(Base64.getEncoder().encode(Utf8.encode(keyPayload)));
}
@Override
public Token verifyToken(String key) {
if (key == null || "".equals(key)) {
return null;
}
String[] tokens = StringUtils
.delimitedListToStringArray(Utf8.decode(Base64.getDecoder().decode(Utf8.encode(key))), ":");
Assert.isTrue(tokens.length >= 4, () -> "Expected 4 or more tokens but found " + tokens.length);
long creationTime;
try {
creationTime = Long.decode(tokens[0]);
}
catch (NumberFormatException ex) {
throw new IllegalArgumentException("Expected number but found " + tokens[0]);
}
String serverSecret = computeServerSecretApplicableAt(creationTime);
String pseudoRandomNumber = tokens[1];
// Permit extendedInfo to itself contain ":" characters
StringBuilder extendedInfo = new StringBuilder();
for (int i = 2; i < tokens.length - 1; i++) {
if (i > 2) {
extendedInfo.append(":");
}
extendedInfo.append(tokens[i]);
}
String sha1Hex = tokens[tokens.length - 1];
// Verification
String content = creationTime + ":" + pseudoRandomNumber + ":" + extendedInfo.toString();
String expectedSha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret);
Assert.isTrue(expectedSha512Hex.equals(sha1Hex), "Key verification failure");
return new DefaultToken(key, creationTime, extendedInfo.toString());
}
/**
* @return a pseduo random number (hex encoded)
*/
private String generatePseudoRandomNumber() {
byte[] randomBytes = new byte[this.pseudoRandomNumberBytes];
this.secureRandom.nextBytes(randomBytes);
return new String(Hex.encode(randomBytes));
}
private String computeServerSecretApplicableAt(long time) {
return this.serverSecret + ":" + new Long(time % this.serverInteger).intValue();
}
/**
* @param serverSecret the new secret, which can contain a ":" if desired (never being
* sent to the client)
*/
public void setServerSecret(String serverSecret) {
this.serverSecret = serverSecret;
}
public void setSecureRandom(SecureRandom secureRandom) {
this.secureRandom = secureRandom;
}
/**
* @param pseudoRandomNumberBytes changes the number of bytes issued (must be >= 0;
* defaults to 256)
*/
public void setPseudoRandomNumberBytes(int pseudoRandomNumberBytes) {
Assert.isTrue(pseudoRandomNumberBytes >= 0, "Must have a positive pseudo random number bit size");
this.pseudoRandomNumberBytes = pseudoRandomNumberBytes;
}
public void setServerInteger(Integer serverInteger) {
this.serverInteger = serverInteger;
}
@Override
public void afterPropertiesSet() {
Assert.hasText(this.serverSecret, "Server secret required");
Assert.notNull(this.serverInteger, "Server integer required");
Assert.notNull(this.secureRandom, "SecureRandom instance required");
}
}